diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ab1e585611ad..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,37 +0,0 @@ -### Is this a *feature* OR *bug*: - - - -### System Details - -- Operating system name and version: -- Output from `$PSVersionTable`: - -``` -Evaluate $PSVersionTable in PowerShell and paste the output here -``` - -- Output of dbatools version: - -``` -Evaluate (Get-Module dbatools -ListAvailable).Version and paste output here -``` - -- SQL Server version for source/target - - -### Steps to Reproduce - - - -### Action Results - - - -### Expected Results - - -### Attached Logs or any exception errors - -- Use of Start-Transcript can help collection of console output and exceptions -- Certain commands will generate an exception log that you can find in following directory: `dir $env:USERPROFILE\Documents\dbatools-exceptions.txt` diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md new file mode 100644 index 000000000000..25d91eae1eaa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -0,0 +1,45 @@ +--- +name: "Bug report \U0001F41B" +about: Found errors or unexpected behavior using dbatools module + +--- + +### Before submitting a bug report: + +- [ ] Ensure you are able to reproduce it on the latest released version (_we release often_) +- [ ] Verified this bug is not already reported in an issue +- [ ] Verified errors are not related to permissions +- [ ] Can reproduce in a clean PowerShell session (_clean = `powershell -NoProfile`_) + +## Steps to Reproduce + +```sql +/* + Any T-SQL commands involved or used to produce test objects/data. +*/ +``` + +```powershell +<# + Provide exact (or sanitized) code to reproduce the error +#> +``` + +## Expected Behavior + + + +## Actual Behavior + + + +## Environmental data + + + + - PowerShell: + - - SQL Server: diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 000000000000..4e5f5fbe930d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +--- +name: Feature request/idea :bomb: +about: Suggest a new feature or enhancement + +--- + +### Summary of new feature + +* Clear/concise description of what the problem is that the new feature can solve. + +### Proposed technical details (if applicable) + +* e.g. Blog post reference that shows example code or functionality + +### Latest version of dbatools as of writing + +* Please provide the latest released version of dbatools module. diff --git a/.github/ISSUE_TEMPLATE/Support_Question.md b/.github/ISSUE_TEMPLATE/Support_Question.md new file mode 100644 index 000000000000..96758bed6b7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Support_Question.md @@ -0,0 +1,11 @@ +--- +name: Support Question ❓ +about: If you have a question you may try asking on StackExchange network or dbatools + channel in sqlcommunity.slack.com + +--- + +- Slack Community Channel: https://sqlcommunity.slack.com/messages/C1M2WEASG/ (get an invite: https://dbatools.io/slack/) +- Documentation within the module: `Get-Help ` +- Database Administrator on Stack Exchange network, dbatools tag: https://dba.stackexchange.com/questions/tagged/dbatools +- StackOverflow dbatools tag: https://stackoverflow.com/questions/tagged/dbatools diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ee6c71e190d4..fe27d609b3ce 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,35 +1,34 @@ -Fixes # + +## Type of Change + + - [ ] Bug fix (non-breaking change, fixes #) + - [ ] New feature (non-breaking change, adds functionality) + - [ ] Breaking change (effects multiple commands or functionality) + - [ ] Ran manual Pester test and has passed (`.\tests\manual.pester.ps1) + - [ ] Adding code coverage to existing functionality + - [ ] Pester test is included + - [ ] Nunit test is included + - [ ] Documentation + - [ ] Build system + + +### Purpose + -Changes proposed in this pull request: - - - - - - +### Approach + -How to test this code: -- [ ] -- [ ] +### Commands to test + -Has been tested on minimum requirements: -- [ ] Powershell 3 -- [ ] Windows 7 -- [ ] SQL Server 2000 - -Has been tested on maximum requirements: -- [ ] SQL Server vNext -- [ ] Windows 10 -- [ ] Azure Database - -Tests for tester: -- [ ] Working/useful help content, including link to command on dbatools web site -- [ ] All examples work as advertised -- [ ] Does not contain template content -- [ ] Does not contain excessive/unnecessary amounts of comments -- [ ] Works remotely -- [ ] Works locally -- [ ] Works on lower versions or throws error specifying version not supported -- [ ] Works with named instances -- [ ] Works with clustered instances -- [ ] Handles offline/read only databases -- [ ] Supports multiple servers (at the command line or piped from Get-SqlRegisteredServerName) -- [ ] No un-handled errors which stop the command working with multiple servers +### Screenshots + +### Learning + + diff --git a/.gitignore b/.gitignore index a6b46b7aa8ff..008b6971a578 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,22 @@ -dbatools-startmigration-transcript.txt -dbatools-exceptions.txt -*TempPoint* -*.sln -*.pssproj - -# ignore the settings folder and files for VSCode -.vscode/* +dbatools-startmigration-transcript.txt +dbatools-exceptions.txt +*TempPoint* +*.pssproj + +# ignore the settings folder and files for VSCode +.vscode/* +*.psproj +dbatools.psprojs + +# ignore the settings folder and files for Visual Studio +.vs/* + +# VIM backup files +*~ + +# msbuild log file +msbuild.log + +# Local constant file +tests/constants.local.ps1 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..b019d01ffc79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Chrissy LeMaire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index b494469d17d5..000000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/allcommands.ps1 b/allcommands.ps1 new file mode 100644 index 000000000000..deb6195df413 --- /dev/null +++ b/allcommands.ps1 @@ -0,0 +1,94469 @@ +### DO NOT EDIT THIS FILE DIRECTLY ### +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Add-DbaComputerCertificate { + <# + .SYNOPSIS + Adds a computer certificate - useful for older systems. + + .DESCRIPTION + Adds a computer certificate from a local or remote computer. + + .PARAMETER ComputerName + The target SQL Server. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. + + .PARAMETER Password + The password for the certificate, if it is password protected. + + .PARAMETER Certificate + The target certificate object. + + .PARAMETER Path + The local path to the target certificate object. + + .PARAMETER Store + Certificate store. Default is LocalMachine. + + .PARAMETER Folder + Certificate folder. Default is My (Personal). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Add-DbaComputerCertificate -ComputerName Server1 -Path C:\temp\cert.cer + + Adds the local C:\temp\cert.cer to the remote server Server1 in LocalMachine\My (Personal). + + .EXAMPLE + Add-DbaComputerCertificate -Path C:\temp\cert.cer + + Adds the local C:\temp\cert.cer to the local computer's LocalMachine\My (Personal) certificate store. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param ( + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [securestring]$Password, + [parameter(ValueFromPipeline)] + [System.Security.Cryptography.X509Certificates.X509Certificate2[]]$Certificate, + [string]$Path, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if ($Path) { + if (!(Test-Path -Path $Path)) { + Stop-Function -Message "Path ($Path) does not exist." -Category InvalidArgument + return + } + + try { + # This may be too much, but ¯\_(ツ)_/¯ + $bytes = [System.IO.File]::ReadAllBytes($Path) + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $Certificate.Import($bytes, $Password, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) + } + catch { + Stop-Function -Message "Can't import certificate." -ErrorRecord $_ + return + } + } + + #region Remoting Script + $scriptBlock = { + + param ( + $CertificateData, + + [securestring]$Password, + + $Store, + + $Folder + ) + + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($CertificateData, $Password, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) + Write-Message -Level Verbose -Message "Importing cert to $Folder\$Store" + $tempStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($Folder, $Store) + $tempStore.Open('ReadWrite') + $tempStore.Add($cert) + $tempStore.Close() + + Write-Message -Level Verbose -Message "Searching Cert:\$Store\$Folder" + Get-ChildItem "Cert:\$Store\$Folder" -Recurse | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + } + #endregion Remoting Script + } + process { + if (Test-FunctionInterrupt) { return } + + if (-not $Certificate) { + Stop-Function -Message "You must specify either Certificate or Path" -Category InvalidArgument + return + } + + foreach ($cert in $Certificate) { + + try { + $certData = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::PFX, $Password) + } + catch { + Stop-Function -Message "Can't export certificate" -ErrorRecord $_ -Continue + } + + foreach ($computer in $ComputerName) { + + if ($PScmdlet.ShouldProcess("local", "Connecting to $computer to import cert")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ArgumentList $certdata, $Password, $Store, $Folder -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + } + } +} +function Add-DbaPfDataCollectorCounter { + <# + .SYNOPSIS + Adds a Performance Data Collector Counter. + + .DESCRIPTION + Adds a Performance Data Collector Counter. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER Counter + The Counter name. This must be in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollector via the pipeline. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Add-DbaPfDataCollectorCounter + + .EXAMPLE + Add-DbaPfDataCollectorCounter -ComputerName sql2017 -CollectorSet 'System Correlation' -Collector DataCollector01 -Counter '\LogicalDisk(*)\Avg. Disk Queue Length' + + Adds the '\LogicalDisk(*)\Avg. Disk Queue Length' counter within the DataCollector01 collector within the System Correlation collector set on sql2017. + + .EXAMPLE + Get-DbaPfDataCollector | Out-GridView -PassThru | Add-DbaPfDataCollectorCounter -Counter '\LogicalDisk(*)\Avg. Disk Queue Length' -Confirm + + Allows you to select which Data Collector you'd like to add the counter '\LogicalDisk(*)\Avg. Disk Queue Length' on localhost and prompts for confirmation. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [Alias("Name")] + [parameter(Mandatory, ValueFromPipelineByPropertyName)] + [object[]]$Counter, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $setscript = { + $setname = $args[0]; $Addxml = $args[1] + $set = New-Object -ComObject Pla.DataCollectorSet + $set.SetXml($Addxml) + $set.Commit($setname, $null, 0x0003) #add or modify. + $set.Query($setname, $Null) + } + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (($InputObject | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue).Count -le 3 -and $InputObject.ComputerName -and $InputObject.Name) { + # it's coming from Get-DbaPfAvailableCounter + $ComputerName = $InputObject.ComputerName + $Counter = $InputObject.Name + $InputObject = $null + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollector -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollector or Get-DbaPfAvailableCounter." + return + } + } + + foreach ($object in $InputObject) { + $computer = $InputObject.ComputerName + $null = Test-ElevationRequirement -ComputerName $computer -Continue + $setname = $InputObject.DataCollectorSet + $collectorname = $InputObject.Name + $xml = [xml]($InputObject.DataCollectorSetXml) + + foreach ($countername in $counter) { + $node = $xml.SelectSingleNode("//Name[.='$collectorname']") + $newitem = $xml.CreateElement('Counter') + $null = $newitem.PsBase.InnerText = $countername + $null = $node.ParentNode.AppendChild($newitem) + $newitem = $xml.CreateElement('CounterDisplayName') + $null = $newitem.PsBase.InnerText = $countername + $null = $node.ParentNode.AppendChild($newitem) + } + $plainxml = $xml.OuterXml + + if ($Pscmdlet.ShouldProcess("$computer", "Adding $counters to $collectorname with the $setname collection set")) { + try { + $results = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $setname, $plainxml -ErrorAction Stop + Write-Message -Level Verbose -Message " $results" + Get-DbaPfDataCollectorCounter -ComputerName $computer -Credential $Credential -CollectorSet $setname -Collector $collectorname -Counter $counter + } + catch { + Stop-Function -Message "Failure importing $Countername to $computer." -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} +function Backup-DbaDatabase { + <# + .SYNOPSIS + Backup one or more SQL Sever databases from a single SQL Server SqlInstance. + + .DESCRIPTION + Performs a backup of a specified type of 1 or more databases on a single SQL Server Instance. These backups may be Full, Differential or Transaction log backups. + + .PARAMETER SqlInstance + The SQL Server instance hosting the databases to be backed up. + + .PARAMETER SqlCredential + Credentials to connect to the SQL Server instance if the calling user doesn't have permission. + + .PARAMETER Database + The database(s) to process. This list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. This list is auto-populated from the server. + + .PARAMETER BackupFileName + The name of the file to backup to. This is only accepted for single database backups. + If no name is specified then the backup files will be named DatabaseName_yyyyMMddHHmm (i.e. "Database1_201714022131") with the appropriate extension. + + If the same name is used repeatedly, SQL Server will add backups to the same file at an incrementing position. + + SQL Server needs permissions to write to the specified location. Path names are based on the SQL Server (C:\ is the C drive on the SQL Server, not the machine running the script). + + .PARAMETER BackupDirectory + Path in which to place the backup files. If not specified, the backups will be placed in the default backup location for SqlInstance. + If multiple paths are specified, the backups will be striped across these locations. This will overwrite the FileCount option. + + If the path does not exist, Sql Server will attempt to create it. Folders are created by the Sql Instance, and checks will be made for write permissions. + + File Names with be suffixed with x-of-y to enable identifying striped sets, where y is the number of files in the set and x ranges from 1 to y. + + .PARAMETER CopyOnly + If this switch is enabled, CopyOnly backups will be taken. By default function performs a normal backup, these backups interfere with the restore chain of the database. CopyOnly backups will not interfere with the restore chain of the database. + + For more details please refer to this MSDN article - https://msdn.microsoft.com/en-us/library/ms191495.aspx + + .PARAMETER Type + The type of SQL Server backup to perform. Accepted values are "Full", "Log", "Differential", "Diff", "Database" + + .PARAMETER FileCount + This is the number of striped copies of the backups you wish to create. This value is overwritten if you specify multiple Backup Directories. + + .PARAMETER CreateFolder + If this switch is enabled, each database will be backed up into a separate folder on each of the paths specified by BackupDirectory. + + .PARAMETER CompressBackup + If this switch is enabled, the function will try to perform a compressed backup if supported by the version and edition of SQL Server. Otherwise, this function will use the server's default setting for compression. + + .PARAMETER MaxTransferSize + Sets the size of the unit of transfer. Values must be a multiple of 64kb. + + .PARAMETER Blocksize + Specifies the block size to use. Must be one of 0.5KB, 1KB, 2KB, 4KB, 8KB, 16KB, 32KB or 64KB. This can be specified in bytes. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER BufferCount + Number of I/O buffers to use to perform the operation. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER Checksum + If this switch is enabled, the backup checksum will be calculated. + + .PARAMETER Verify + If this switch is enabled, the backup will be verified by running a RESTORE VERIFYONLY against the SqlInstance + + .PARAMETER InputObject + Internal parameter + + .PARAMETER AzureBaseUrl + The URL to the basecontainer of an Azure storage account to write backups to. + + If specified, the only other parameters than can be used are "NoCopyOnly", "Type", "CompressBackup", "Checksum", "Verify", "AzureCredential", "CreateFolder". + + .PARAMETER AzureCredential + The name of the credential on the SQL instance that can write to the AzureBaseUrl. + + .PARAMETER NoRecovery + This is passed in to perform a tail log backup if needed + + .PARAMETER BuildPath + By default this command won't attempt to create missing paths, this switch will change the behavious so that it wll + + .PARAMETER IgnoreFileChecks + This switch stops the function from checking for the validity of paths. This can be useful if SQL Server only has read access to the backup area. + Note, that as we can't check the path you may well end up with errors. + + .PARAMETER OutputScriptOnly + Switch causes only the T-SQL script for the backup to be generated. Will not create any paths if they do not exist + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Author: Stuart Moore (@napalmgram), stuart-moore.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Backup-DbaDatabase -SqlInstance Server1 -Database HR, Finance + + This will perform a full database backup on the databases HR and Finance on SQL Server Instance Server1 to Server1's default backup directory. + + .EXAMPLE + Backup-DbaDatabase -SqlInstance sql2016 -BackupDirectory C:\temp -Database AdventureWorks2014 -Type Full + + Backs up AdventureWorks2014 to sql2016's C:\temp folder. + + .EXAMPLE + Backup-DbaDatabase -SqlInstance sql2016 -AzureBaseUrl https://dbatoolsaz.blob.core.windows.net/azbackups/ -AzureCredential dbatoolscred -Type Full -CreateFolder + + Performs a full backup of all databases on the sql2016 instance to their own containers under the https://dbatoolsaz.blob.core.windows.net/azbackups/ container on Azure blog storage using the sql credential "dbatoolscred" registered on the sql2016 instance. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] #For AzureCredential + param ( + [parameter(ParameterSetName = "Pipe", Mandatory = $true)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [string[]]$BackupDirectory, + [string]$BackupFileName, + [switch]$CopyOnly, + [ValidateSet('Full', 'Log', 'Differential', 'Diff', 'Database')] + [string]$Type = 'Database', + [parameter(ParameterSetName = "NoPipe", Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$InputObject, + [switch]$CreateFolder, + [int]$FileCount = 0, + [switch]$CompressBackup, + [switch]$Checksum, + [switch]$Verify, + [int]$MaxTransferSize, + [int]$BlockSize, + [int]$BufferCount, + [string]$AzureBaseUrl, + [string]$AzureCredential, + [switch]$NoRecovery, + [switch]$BuildPath, + [switch]$IgnoreFileChecks, + [switch]$OutputScriptOnly, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if ($SqlInstance.length -ne 0) { + Write-Message -Level Verbose -Message "Connecting to $SqlInstance" + try { + $Server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Cannot connect to $SqlInstance" + continue + } + + if ($Database) { + $InputObject = $server.Databases | Where-Object Name -in $Database + } + else { + $InputObject = $server.Databases | Where-Object Name -ne 'tempdb' + } + + if ($ExcludeDatabase) { + $InputObject = $InputObject | Where-Object Name -notin $ExcludeDatabase + } + + if ($BackupDirectory.Count -gt 1) { + Write-Message -Level Verbose -Message "Multiple Backup Directories, striping" + $Filecount = $BackupDirectory.Count + } + + if ($InputObject.Count -gt 1 -and $BackupFileName -ne '') { + Write-Message -Level Warning -Message "1 BackupFile specified, but more than 1 database." + break + } + + if (($MaxTransferSize % 64kb) -ne 0 -or $MaxTransferSize -gt 4mb) { + Write-Message -Level Warning -Message "MaxTransferSize value must be a multiple of 64kb and no greater than 4MB" + break + } + if ($BlockSize) { + if ($BlockSize -notin (0.5kb, 1kb, 2kb, 4kb, 8kb, 16kb, 32kb, 64kb)) { + Write-Message -Level Warning -Message "Block size must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb,64kb" + break + } + } + if ('' -ne $AzureBaseUrl) { + if ($null -eq $AzureCredential) { + Stop-Function -Message "You must provide the credential name for the Azure Storage Account" + break + } + $AzureBaseUrl = $AzureBaseUrl.Trim("/") + $FileCount = 1 + $BackupDirectory = $AzureBaseUrl + } + + if ($OutputScriptOnly) { + $IgnoreFileChecks = $true + } + } + } + + process { + if (!$SqlInstance -and !$InputObject) { + Write-Message -Level Warning -Message "You must specify a server and database or pipe some databases" + continue + } + + Write-Message -Level Verbose -Message "$($InputObject.Count) database to backup" + + foreach ($Database in $InputObject) { + $ProgressId = Get-Random + $failures = @() + $dbname = $Database.Name + + if ($dbname -eq "tempdb") { + Write-Message -Level Warning -Message "Backing up tempdb not supported" + continue + } + + if ('Normal' -notin ($Database.Status -split ',')) { + Write-Message -Level Warning -Message "Database status not Normal. $dbname skipped." + continue + } + + if ($Database.DatabaseSnapshotBaseName) { + Write-Message -Level Warning -Message "Backing up snapshots not supported. $dbname skipped." + continue + } + + if ($null -eq $server) { $server = $Database.Parent } + + Write-Message -Level Verbose -Message "Backup database $database" + + if ($null -eq $Database.RecoveryModel) { + $Database.RecoveryModel = $server.Databases[$Database.Name].RecoveryModel + Write-Message -Level Verbose -Message "$dbname is in $($Database.RecoveryModel) recovery model" + } + + # Fixes one-off cases of StackOverflowException crashes, see issue 1481 + $dbRecovery = $Database.RecoveryModel.ToString() + if ($dbRecovery -eq 'Simple' -and $Type -eq 'Log') { + $failreason = "$database is in simple recovery mode, cannot take log backup" + $failures += $failreason + Write-Message -Level Warning -Message "$failreason" + } + + $lastfull = $database.Refresh().LastBackupDate.Year + + if ($Type -notin @("Database", "Full") -and $lastfull -eq 1) { + $failreason = "$database does not have an existing full backup, cannot take log or differentialbackup" + $failures += $failreason + Write-Message -Level Warning -Message "$failreason" + } + + if ($CopyOnly -ne $true) { + $CopyOnly = $false + } + + $server.ConnectionContext.StatementTimeout = 0 + $backup = New-Object Microsoft.SqlServer.Management.Smo.Backup + $backup.Database = $Database.Name + $Suffix = "bak" + + if ($CompressBackup) { + if ($server.Edition -like 'Express*' -or ($server.VersionMajor -eq 10 -and $server.VersionMinor -eq 0 -and $server.Edition -notlike '*enterprise*') -or $server.VersionMajor -lt 10) { + Write-Message -Level Warning -Message "Compression is not supported with this version/edition of Sql Server" + } + else { + Write-Message -Level Verbose -Message "Compression enabled" + $backup.CompressionOption = 1 + } + } + + if ($Checksum) { + $backup.Checksum = $true + } + + if ($Type -in 'Diff', 'Differential') { + Write-Message -Level VeryVerbose -Message "Creating differential backup" + $SMOBackuptype = "Database" + $backup.Incremental = $true + $outputType = 'Differential' + } + $Backup.NoRecovery = $false + if ($Type -eq "Log") { + Write-Message -Level VeryVerbose -Message "Creating log backup" + $Suffix = "trn" + $OutputType = 'Log' + $SMOBackupType = 'Log' + $Backup.NoRecovery = $NoRecovery + } + + if ($Type -in 'Full', 'Database') { + Write-Message -Level VeryVerbose -Message "Creating full backup" + $SMOBackupType = "Database" + $OutputType = 'Full' + } + + $backup.CopyOnly = $copyonly + $backup.Action = $SMOBackupType + if ('' -ne $AzureBaseUrl) { + $backup.CredentialName = $AzureCredential + } + + Write-Message -Level Verbose -Message "Building file name" + + $BackupFinalName = '' + $FinalBackupPath = @() + if ('NUL' -eq $BackupFileName) { + $FinalBackupPath += 'NUL:' + $IgnoreFileChecks = $true + } + elseif ('' -ne $BackupFileName) { + $File = New-Object System.IO.FileInfo($BackupFileName) + $BackupFinalName = $file.Name + $suffix = $file.extension -Replace '^\.','' + if ( '' -ne (Split-Path $BackupFileName)) { + Write-Message -Level Verbose -Message "Fully qualified path passed in" + $FinalBackupPath += [IO.Path]::GetFullPath($file.DirectoryName) + } + } + else { + $timestamp = (Get-Date -Format yyyyMMddHHmm) + Write-Message -Level VeryVerbose -Message "Setting filename" + $BackupFinalName = "$($dbname)_$timestamp.$suffix" + } + + Write-Message -Level Verbose -Message "Building backup path" + if ($FinalBackupPath.Count -eq 0) { + $FinalBackupPath += $BackupDirectory + } + + if ($BackupDirectory.Count -eq 1 -and $Filecount -gt 1) { + for ($i = 0; $i -lt ($Filecount - 1); $i++) { + $FinalBackupPath += $FinalBackupPath[0] + } + } + + if ($FinalBackupPath.Count -gt 1) { + $File = New-Object System.IO.FileInfo($BackupFinalName) + for ($i = 0; $i -lt $FinalBackupPath.Count; $i++) { + $FinalBackupPath[$i] = $FinalBackupPath[$i] + "\" + $($File.BaseName) + "-$($i+1)-of-$FileCount.$suffix" + } + } + elseif ($FinalBackupPath[0] -ne 'NUL:') { + $FinalBackupPath[0] = $FinalBackupPath[0] + "\" + $BackupFinalName + } + + if ($CreateFolder -and $FinalBackupPath[0] -ne 'NUL:') { + for ($i = 0; $i -lt $FinalBackupPath.Count; $i++) { + $parent = [IO.Path]::GetDirectoryName($FinalBackupPath[$i]) + $leaf = [IO.Path]::GetFileName($FinalBackupPath[$i]) + $FinalBackupPath[$i] = [IO.Path]::Combine($parent, $dbname, $leaf) + } + } + + if (-not $IgnoreFileChecks -and -not $AzureBaseUrl) { + $parentPaths = ($FinalBackupPath | ForEach-Object { Split-Path $_ } | Select-Object -Unique) + foreach ($parentPath in $parentPaths) { + if (-not (Test-DbaSqlPath -SqlInstance $server -Path $parentPath)) { + if (($BuildPath -eq $true) -or ($CreateFolder -eq $True)) { + $null = New-DbaSqlDirectory -SqlInstance $server -Path $parentPath + } + else { + $failreason += "SQL Server cannot check if $parentPath exists. You can try disabiling this check with -IgnoreFileChecks" + $failures += $failreason + Write-Message -Level Warning -Message "$failreason" + } + } + } + } + + + if ('' -eq $AzureBaseUrl -and $BackupDirectory) { + $FinalBackupPath = $FinalBackupPath | ForEach-Object { [IO.Path]::GetFullPath($_) } + } + + + $script = $null + $backupComplete = $false + + if (!$failures) { + $Filecount = $FinalBackupPath.Count + + foreach ($backupfile in $FinalBackupPath) { + $device = New-Object Microsoft.SqlServer.Management.Smo.BackupDeviceItem + if ('' -ne $AzureBaseUrl) { + $device.DeviceType = "URL" + } + else { + $device.DeviceType = "File" + } + $device.Name = $backupfile + $backup.Devices.Add($device) + } + $humanBackupFile = $FinalBackupPath -Join ',' + Write-Message -Level Verbose -Message "Devices added" + $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id $ProgressId -activity "Backing up database $dbname to $humanBackupFile" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $backup.add_PercentComplete($percent) + $backup.PercentCompleteNotification = 1 + $backup.add_Complete($complete) + + if ($MaxTransferSize) { + $backup.MaxTransferSize = $MaxTransferSize + } + if ($BufferCount) { + $backup.BufferCount = $BufferCount + } + if ($BlockSize) { + $backup.Blocksize = $BlockSize + } + + Write-Progress -id $ProgressId -activity "Backing up database $dbname to $humanBackupFile" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + + try { + if ($Pscmdlet.ShouldProcess($server.Name, "Backing up $dbname to $humanBackupFile")) { + if ($OutputScriptOnly -ne $True) { + $Filelist = @() + $FileList += $server.Databases[$dbname].FileGroups.Files | Select-Object @{ Name = "FileType"; Expression = { "D" } }, @{ Name = "Type"; Expression = { "D" } }, @{ Name = "LogicalName"; Expression = { $_.Name } }, @{ Name = "PhysicalName"; Expression = { $_.FileName } } + $FileList += $server.Databases[$dbname].LogFiles | Select-Object @{ Name = "FileType"; Expression = { "L" } }, @{ Name = "Type"; Expression = { "L" } }, @{ Name = "LogicalName"; Expression = { $_.Name } }, @{ Name = "PhysicalName"; Expression = { $_.FileName } } + + $backup.SqlBackup($server) + $script = $backup.Script($server) + Write-Progress -id $ProgressId -activity "Backing up database $dbname to $backupfile" -status "Complete" -Completed + $BackupComplete = $true + if ($server.VersionMajor -eq '8') { + $HeaderInfo = Get-BackupAncientHistory -SqlInstance $server -Database $dbname + } + else { + $HeaderInfo = Get-DbaBackupHistory -SqlInstance $server -Database $dbname -Last -IncludeCopyOnly | Sort-Object -Property End -Descending | Select-Object -First 1 + } + $Verified = $false + if ($Verify) { + $verifiedresult = [PSCustomObject]@{ + SqlInstance = $server.name + DatabaseName = $dbname + BackupComplete = $BackupComplete + BackupFilesCount = $FinalBackupPath.Count + BackupFile = (Split-Path $FinalBackupPath -Leaf) + BackupFolder = (Split-Path $FinalBackupPath | Sort-Object -Unique) + BackupPath = ($FinalBackupPath | Sort-Object -Unique) + Script = $script + Notes = $failures -join (',') + FullName = ($FinalBackupPath | Sort-Object -Unique) + FileList = $FileList + SoftwareVersionMajor = $server.VersionMajor + Type = $outputType + FirstLsn = $HeaderInfo.FirstLsn + DatabaseBackupLsn = $HeaderInfo.DatabaseBackupLsn + CheckPointLsn = $HeaderInfo.CheckPointLsn + LastLsn = $HeaderInfo.LastLsn + BackupSetId = $HeaderInfo.BackupSetId + LastRecoveryForkGUID = $HeaderInfo.LastRecoveryForkGUID + } | Restore-DbaDatabase -SqlInstance $server -DatabaseName DbaVerifyOnly -VerifyOnly -TrustDbBackupHistory -DestinationFilePrefix DbaVerifyOnly + if ($verifiedResult[0] -eq "Verify successful") { + $failures += $verifiedResult[0] + $Verified = $true + } + else { + $failures += $verifiedResult[0] + $Verified = $false + } + } + $HeaderInfo | Add-Member -Type NoteProperty -Name BackupComplete -Value $BackupComplete + $HeaderInfo | Add-Member -Type NoteProperty -Name BackupFile -Value (Split-Path $FinalBackupPath -Leaf) + $HeaderInfo | Add-Member -Type NoteProperty -Name BackupFilesCount -Value $FinalBackupPath.Count + if ($FinalBackupPath[0] -eq 'NUL:') { + $pathresult = "NUL:" + } + else { + $pathresult = (Split-Path $FinalBackupPath | Sort-Object -Unique) + } + $HeaderInfo | Add-Member -Type NoteProperty -Name BackupFolder -Value $pathresult + $HeaderInfo | Add-Member -Type NoteProperty -Name BackupPath -Value ($FinalBackupPath | Sort-Object -Unique) + $HeaderInfo | Add-Member -Type NoteProperty -Name DatabaseName -Value $dbname + $HeaderInfo | Add-Member -Type NoteProperty -Name Notes -Value ($failures -join (',')) + $HeaderInfo | Add-Member -Type NoteProperty -Name Script -Value $script + $HeaderInfo | Add-Member -Type NoteProperty -Name Verified -Value $Verified + } + else { + $backup.Script($server) + } + } + } + catch { + if ($NoRecovery -and ($_.Exception.InnerException.InnerException.InnerException -like '*cannot be opened. It is in the middle of a restore.')) { + Write-Message -Message "Exception thrown by db going into restoring mode due to recovery" -Leve Verbose + } + else { + Write-Progress -id $ProgressId -activity "Backup" -status "Failed" -completed + Stop-Function -message "Backup Failed: $($_.Exception.Message)" -EnableException $EnableException -ErrorRecord $_ + $BackupComplete = $false + } + } + } + $OutputExclude = 'FullName', 'FileList', 'SoftwareVersionMajor' + if ($failures.Count -eq 0) { + $OutputExclude += ('Notes', 'FirstLsn', 'DatabaseBackupLsn', 'CheckpointLsn', 'LastLsn', 'BackupSetId', 'LastRecoveryForkGuid') + } + $headerinfo | Select-DefaultView -ExcludeProperty $OutputExclude + $BackupFileName = $null + } + } +} +function Backup-DbaDatabaseMasterKey { + <# +.SYNOPSIS +Backs up specified database master key. + +.DESCRIPTION +Backs up specified database master key. + +.PARAMETER SqlInstance +The target SQL Server instance. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +Backup master key from specific database(s). + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server. + +.PARAMETER Path +The directory to export the key. If no path is specified, the default backup directory for the instance will be used. + +.PARAMETER Password +The password to encrypt the exported key. This must be a SecureString. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate, Database + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Backup-DbaDatabaseMasterKey -SqlInstance server1\sql2016 + +Prompts for export password, then logs into server1\sql2016 with Windows credentials then backs up all database keys to the default backup directory. + +ComputerName : SERVER1 +InstanceName : SQL2016 +SqlInstance : SERVER1\SQL2016 +Database : master +Filename : E:\MSSQL13.SQL2016\MSSQL\Backup\server1$sql2016-master-20170614162311.key +Status : Success + +.EXAMPLE +Backup-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 -Path \\nas\sqlbackups\keys + +Logs into sql2016 with Windows credentials then backs up db1's keys to the \\nas\sqlbackups\keys directory. + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Security.SecureString]$Password, + [string]$Path, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + if (Test-Bound -ParameterName Path -Not) { + $Path = $server.BackupDirectory + } + + if (!$Path) { + Stop-Function -Message "Path discovery failed. Please explicitly specify -Path" -Target $server -Continue + } + + if (!(Test-DbaSqlPath -SqlInstance $server -Path $Path)) { + Stop-Function -Message "$instance cannot access $Path" -Target $server -InnerErrorRecord $_ -Continue + } + + foreach ($db in $databases) { + + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + $masterkey = $db.MasterKey + + if (!$masterkey) { + Write-Message -Message "No master key exists in the $db database on $instance" -Target $db -Level Verbose + continue + } + + # If you pass a password param, then you will not be prompted for each database, but it wouldn't be a good idea to build in insecurity + if (Test-Bound -ParameterName Password -Not) { + $password = Read-Host -AsSecureString -Prompt "You must enter Service Key password for $instance" + $password2 = Read-Host -AsSecureString -Prompt "Type the password again" + + if (([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) -ne ([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password2)))) { + Stop-Function -Message "Passwords do not match" -Continue + } + } + + $time = (Get-Date -Format yyyMMddHHmmss) + $dbname = $db.name + $Path = $Path.TrimEnd("\") + $fileinstance = $instance.ToString().Replace('\', '$') + $filename = "$Path\$fileinstance-$dbname-$time.key" + + try { + $masterkey.export($filename, [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) + $status = "Success" + } + catch { + $status = "Failure" + Write-Message -Level Warning -Message "Backup failure: $($_.Exception.InnerException)" + } + + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Database -value $dbname + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Filename -value $filename + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Status -value $status + + Select-DefaultView -InputObject $masterkey -Property ComputerName, InstanceName, SqlInstance, Database, 'Filename as Path', Status + } + } + } +} +function Backup-DbaDbCertificate { + <# + .SYNOPSIS + Exports database certificates from SQL Server using SMO. + + .DESCRIPTION + Exports database certificates from SQL Server using SMO and outputs the .cer and .pvk files. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Certificate + Exports certificate that matches the name(s). + + .PARAMETER Database + Exports the encryptor for specific database(s). + + .PARAMETER ExcludeDatabase + Database(s) to skip when exporting encryptors. + + .PARAMETER EncryptionPassword + A string value that specifies the system path to encrypt the private key. + + .PARAMETER DecryptionPassword + A string value that specifies the system path to decrypt the private key. + + .PARAMETER Path + The path to output the files to. The path is relative to the SQL Server itself. If no path is specified, the default data directory will be used. + + .PARAMETER Suffix + The suffix of the filename of the exported certificate. + + .PARAMETER InputObject + Certificate object + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .NOTES + Author: Jess Pomfret (@jpomfret) + Tags: Migration, Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 + Exports all the certificates on the specified SQL Server to the default data path for the instance. + + .EXAMPLE + $cred = Get-Credential sqladmin + Backup-DbaDbCertificate -SqlInstance Server1 -SqlCredential $cred + + Connects using sqladmin credential and exports all the certificates on the specified SQL Server to the default data path for the instance. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -Certificate Certificate1 + Exports only the certificate named Certificate1 on the specified SQL Server to the default data path for the instance. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -Database AdventureWorks + Exports only the certificates for AdventureWorks on the specified SQL Server to the default data path for the instance. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -ExcludeDatabase AdventureWorks + Exports all certificates except those for AdventureWorks on the specified SQL Server to the default data path for the instance. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -Path \\Server1\Certificates -EncryptionPassword (ConvertTo-SecureString -force -AsPlainText GoodPass1234!!) + Exports all the certificates and private keys on the specified SQL Server. + + .EXAMPLE + $EncryptionPassword = ConvertTo-SecureString -AsPlainText "GoodPass1234!!" -force + $DecryptionPassword = ConvertTo-SecureString -AsPlainText "Password4567!!" -force + Backup-DbaDbCertificate -SqlInstance Server1 -EncryptionPassword $EncryptionPassword -DecryptionPassword $DecryptionPassword + Exports all the certificates on the specified SQL Server using the supplied DecryptionPassword, since an EncryptionPassword is specified private keys are also exported. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -Path \\Server1\Certificates + Exports all certificates on the specified SQL Server to the specified path. + + .EXAMPLE + Backup-DbaDbCertificate -SqlInstance Server1 -Suffix DbaTools + Exports all certificates on the specified SQL Server to the specified path, appends DbaTools to the end of the filenames. + + .EXAMPLE + Get-DbaDbCertificate -SqlInstance sql2016 | Backup-DbaDbCertificate + Exports all certificates found on sql2016 to the default data directory. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ParameterSetName = "instance")] + [object[]]$Certificate, + [parameter(ParameterSetName = "instance")] + [object[]]$Database, + [parameter(ParameterSetName = "instance")] + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $false)] + [Security.SecureString]$EncryptionPassword, + [parameter(Mandatory = $false)] + [Security.SecureString]$DecryptionPassword, + [System.IO.FileInfo]$Path, + [string]$Suffix = "$(Get-Date -format 'yyyyMMddHHmmssms')", + [parameter(ValueFromPipeline, ParameterSetName = "collection")] + [Microsoft.SqlServer.Management.Smo.Certificate[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if ($EncryptionPassword.Length -eq 0 -and $DecryptionPassword.Length -gt 0) { + Stop-Function -Message "If you specify an decryption password, you must also specify an encryption password" -Target $DecryptionPassword + } + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Backup-DbaDatabaseCertificate + + function export-cert ($cert) { + $certName = $cert.Name + $db = $cert.Parent + $server = $db.Parent + $instance = $server.Name + $actualPath = $Path + + if ($null -eq $actualPath) { + $actualPath = Get-SqlDefaultPaths -SqlInstance $server -filetype Data + } + + $fullCertName = "$actualPath\$certName$Suffix" + $exportPathKey = "$fullCertName.pvk" + + if (!(Test-DbaSqlPath -SqlInstance $server -Path $actualPath)) { + Stop-Function -Message "$SqlInstance cannot access $actualPath" -Target $actualPath + } + + if ($Pscmdlet.ShouldProcess($instance, "Exporting certificate $certName from $db on $instance to $actualPath")) { + Write-Message -Level Verbose -Message "Exporting Certificate: $certName to $fullCertName" + try { + + $exportPathCert = "$fullCertName.cer" + + # because the password shouldn't go to memory... + if ($EncryptionPassword.Length -gt 0 -and $DecryptionPassword.Length -gt 0) { + + Write-Message -Level Verbose -Message "Both passwords passed in. Will export both cer and pvk." + + $cert.export( + $exportPathCert, + $exportPathKey, + [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($EncryptionPassword)), + [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($DecryptionPassword)) + ) + } + elseif ($EncryptionPassword.Length -gt 0 -and $DecryptionPassword.Length -eq 0) { + Write-Message -Level Verbose -Message "Only encryption password passed in. Will export both cer and pvk." + + $cert.export( + $exportPathCert, + $exportPathKey, + [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($EncryptionPassword)) + ) + } + else { + Write-Message -Level Verbose -Message "No passwords passed in. Will export just cer." + $exportPathKey = "Password required to export key" + $cert.export($exportPathCert) + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Certificate = $certName + Path = $exportPathCert + Key = $exportPathKey + ExportPath = $exportPathCert + ExportKey = $exportPathKey + exportPathCert = $exportPathCert + exportPathKey = $exportPathKey + Status = "Success" + } | Select-DefaultView -ExcludeProperty exportPathCert, exportPathKey, ExportPath, ExportKey + } + catch { + + if ($_.Exception.InnerException) { + $exception = $_.Exception.InnerException.ToString() -Split "System.Data.SqlClient.SqlException: " + $exception = ($exception[1] -Split "at Microsoft.SqlServer.Management.Common.ConnectionManager")[0] + } + else { + $exception = $_.Exception + } + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Certificate = $certName + Path = $exportPathCert + Key = $exportPathKey + ExportPath = $exportPathCert + ExportKey = $exportPathKey + exportPathCert = $exportPathCert + exportPathKey = $exportPathKey + Status = "Failure: $exception" + } | Select-DefaultView -ExcludeProperty exportPathCert, exportPathKey, ExportPath, ExportKey + Stop-Function -Message "$certName from $db on $instance cannot be exported." -Continue -Target $cert -ErrorRecord $_ + } + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + $databases = Get-DbaDatabase -SqlInstance $server | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -in $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + foreach ($db in $databases.Name) { + $DBInputObject = Get-DbaDbCertificate -SqlInstance $server -Database $db + if ($Certificate) { + $InputObject += $DBInputObject | Where-Object Name -In $Certificate + } + else { + $InputObject += $DBInputObject | Where-Object Name -NotLike "##*" + } + if (!$InputObject) { + Write-Message -Level Output -Message "No certificates found to export in $db." + continue + } + } + + } + + foreach ($cert in $InputObject) { + if ($cert.Name.StartsWith("##")) { + Write-Message -Level Output -Message "Skipping system cert $cert" + } + else { + export-cert $cert + } + } + } +} +function Clear-DbaSqlConnectionPool { + <# + .SYNOPSIS + Resets (or empties) the connection pool. + + .DESCRIPTION + This command resets (or empties) the connection pool. + + If there are connections in use at the time of the call, they are marked appropriately and will be discarded (instead of being returned to the pool) when Close() is called on them. + + Ref: https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.clearallpools(v=vs.110).aspx + + .PARAMETER ComputerName + Target computer(s). If no computer name is specified, the local computer is targeted. + + .PARAMETER Credential + Alternate credential object to use for accessing the target computer(s). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Connection + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Clear-DbaSqlConnectionPool + + .EXAMPLE + Clear-DbaSqlConnectionPool + + Clears all local connection pools. + + .EXAMPLE + Clear-DbaSqlConnectionPool -ComputerName workstation27 + + Clears all connection pools on workstation27. +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch][Alias('Silent')] + $EnableException + ) + + process { + # TODO: https://jamessdixon.wordpress.com/2013/01/22/ado-net-and-connection-pooling + + foreach ($computer in $ComputerName) { + try { + if (-not $computer.IsLocalhost) { + Write-Message -Level Verbose -Message "Clearing all pools on remote computer $computer" + if (Test-Bound 'Credential') { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock { [System.Data.SqlClient.SqlConnection]::ClearAllPools() } + } + else { + Invoke-Command2 -ComputerName $computer -ScriptBlock { [System.Data.SqlClient.SqlConnection]::ClearAllPools() } + } + } + else { + Write-Message -Level Verbose -Message "Clearing all local pools" + if (Test-Bound 'Credential') { + Invoke-Command2 -Credential $Credential -ScriptBlock { [System.Data.SqlClient.SqlConnection]::ClearAllPools() } + } + else { + Invoke-Command2 -ScriptBlock { [System.Data.SqlClient.SqlConnection]::ClearAllPools() } + } + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +function Clear-DbaWaitStatistics { + <# + .SYNOPSIS + Clears wait statistics + + .DESCRIPTION + Reset the aggregated statistics - basically just executes DBCC SQLPERF (N'sys.dm_os_wait_stats', CLEAR) + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: WaitStatistic + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Clear-DbaWaitStatistics + + .EXAMPLE + Clear-DbaWaitStatistics -SqlInstance sql2008, sqlserver2012 + After confirmation, clears wait stats on servers sql2008 and sqlserver2012 + + .EXAMPLE + Clear-DbaWaitStatistics -SqlInstance sql2008, sqlserver2012 -Confirm:$false + Clears wait stats on servers sql2008 and sqlserver2012, without prompting + #> + [CmdletBinding(ConfirmImpact = 'High', SupportsShouldProcess)] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Pscmdlet.ShouldProcess($instance, "Performing CLEAR of sys.dm_os_wait_stats")) { + try { + $server.Query("DBCC SQLPERF (N'sys.dm_os_wait_stats', CLEAR);") + $status = "Success" + } + catch { + $status = $_.Exception + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Status = $status + } + } + } + } +} +function Connect-DbaInstance { + <# + .SYNOPSIS + Creates a robust SMO SQL Server object. + + .DESCRIPTION + This command is robust because it initializes properties that do not cause enumeration by default. It also supports both Windows and SQL Server authentication methods, and detects which to use based upon the provided credentials. + + By default, this command also sets the connection's ApplicationName property to "dbatools PowerShell module - dbatools.io - custom connection". If you're doing anything that requires profiling, you can look for this client name. + + Alternatively, you can pass in whichever client name you'd like using the -ClientName parameter. There are a ton of other parameters for you to explore as well. + + See https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.connectionstring.aspx + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnectionstringbuilder.aspx, + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.aspx + + To execute SQL commands, you can use $server.ConnectionContext.ExecuteReader($sql) or $server.Databases['master'].ExecuteNonQuery($sql) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER Credential + Credential object used to connect to the SQL Server Instance as a different user. This can be a Windows or SQL Server account. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Database + The database(s) to process. This list is auto-populated from the server. + + .PARAMETER AccessToken + Gets or sets the access token for the connection. + + .PARAMETER AppendConnectionString + Appends to the current connection string. Note that you cannot pass authentication information using this method. Use -SqlInstance and optionally -SqlCredential to set authentication information. + + .PARAMETER ApplicationIntent + Declares the application workload type when connecting to a server. + + Valid values are "ReadOnly" and "ReadWrite". + + .PARAMETER BatchSeparator + A string to separate groups of SQL statements being executed. By default, this is "GO". + + .PARAMETER ClientName + By default, this command sets the client's ApplicationName property to "dbatools PowerShell module - dbatools.io - custom connection" if you're doing anything that requires profiling, you can look for this client name. Using -ClientName allows you to set your own custom client application name. + + .PARAMETER ConnectTimeout + The length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error. + + Valid values are integers between 0 and 2147483647. + + When opening a connection to a Azure SQL Database, set the connection timeout to 30 seconds. + + .PARAMETER EncryptConnection + If this switch is enabled, SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed. + + For more information, see Connection String Syntax. https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax + + Beginning in .NET Framework 4.5, when TrustServerCertificate is false and Encrypt is true, the server name (or IP address) in a SQL Server SSL certificate must exactly match the server name (or IP address) specified in the connection string. Otherwise, the connection attempt will fail. For information about support for certificates whose subject starts with a wildcard character (*), see Accepted wildcards used by server certificates for server authentication. https://support.microsoft.com/en-us/help/258858/accepted-wildcards-used-by-server-certificates-for-server-authenticati + + .PARAMETER FailoverPartner + The name of the failover partner server where database mirroring is configured. + + If the value of this key is "" (an empty string), then Initial Catalog must be present in the connection string, and its value must not be "". + + The server name can be 128 characters or less. + + If you specify a failover partner but the failover partner server is not configured for database mirroring and the primary server (specified with the Server keyword) is not available, then the connection will fail. + + If you specify a failover partner and the primary server is not configured for database mirroring, the connection to the primary server (specified with the Server keyword) will succeed if the primary server is available. + + + .PARAMETER IsActiveDirectoryUniversalAuth + If this switch is enabled, the connection will be configured to use Azure Active Directory authentication. + + .PARAMETER LockTimeout + Sets the time in seconds required for the connection to time out when the current transaction is locked. + + .PARAMETER MaxPoolSize + Sets the maximum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MinPoolSize + Sets the minimum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MultipleActiveResultSets + If this switch is enabled, an application can maintain multiple active result sets (MARS). + + If this switch is not enabled, an application must process or cancel all result sets from one batch before it can execute any other batch on that connection. + + .PARAMETER MultiSubnetFailover + If this switch is enabled, and your application is connecting to an AlwaysOn availability group (AG) on different subnets, detection of and connection to the currently active server will be faster. For more information about SqlClient support for Always On Availability Groups, see https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/sqlclient-support-for-high-availability-disaster-recovery + + .PARAMETER NetworkProtocol + Explicitly sets the network protocol used to connect to the server. + + Valid values are "TcpIp","NamedPipes","Multiprotocol","AppleTalk","BanyanVines","Via","SharedMemory" and "NWLinkIpxSpx" + + .PARAMETER NonPooledConnection + If this switch is enabled, a non-pooled connection will be requested. + + .PARAMETER PacketSize + Sets the size in bytes of the network packets used to communicate with an instance of SQL Server. Must match at server. + + .PARAMETER PooledConnectionLifetime + When a connection is returned to the pool, its creation time is compared with the current time and the connection is destroyed if that time span (in seconds) exceeds the value specified by Connection Lifetime. This is useful in clustered configurations to force load balancing between a running server and a server just brought online. + + A value of zero (0) causes pooled connections to have the maximum connection timeout. + + .PARAMETER SqlExecutionModes + The SqlExecutionModes enumeration contains values that are used to specify whether the commands sent to the referenced connection to the server are executed immediately or saved in a buffer. + + Valid values include "CaptureSql", "ExecuteAndCaptureSql" and "ExecuteSql". + + .PARAMETER StatementTimeout + Sets the number of seconds a statement is given to run before failing with a timeout error. + + .PARAMETER TrustServerCertificate + When this switch is enabled, the channel will be encrypted while bypassing walking the certificate chain to validate trust. + + .PARAMETER WorkstationId + Sets the name of the workstation connecting to SQL Server. + + .PARAMETER SqlConnectionOnly + Instead of returning a rich SMO server object, this command will only return a SqlConnection object when setting this switch. + + .NOTES + Tags: Connect, Connection + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Connect-DbaInstance + + .EXAMPLE + Connect-DbaInstance -SqlInstance sql2014 + + Creates an SMO Server object that connects using Windows Authentication + + .EXAMPLE + $wincred = Get-Credential ad\sqladmin + Connect-DbaInstance -SqlInstance sql2014 -Credential $wincred + + Creates an SMO Server object that connects using alternative Windows credentials + + .EXAMPLE + $sqlcred = Get-Credential sqladmin + $server = Connect-DbaInstance -SqlInstance sql2014 -Credential $sqlcred + + Login to sql2014 as SQL login sqladmin. + + .EXAMPLE + $server = Connect-DbaInstance -SqlInstance sql2014 -ClientName "my connection" + + Creates an SMO Server object that connects using Windows Authentication and uses the client name "my connection". So when you open up profiler or use extended events, you can search for "my connection". + + .EXAMPLE + $server = Connect-DbaInstance -SqlInstance sql2014 -AppendConnectionString "Packet Size=4096;AttachDbFilename=C:\MyFolder\MyDataFile.mdf;User Instance=true;" + + Creates an SMO Server object that connects to sql2014 using Windows Authentication, then it sets the packet size (this can also be done via -PacketSize) and other connection attributes. + + .EXAMPLE + $server = Connect-DbaInstance -SqlInstance sql2014 -NetworkProtocol TcpIp -MultiSubnetFailover + + Creates an SMO Server object that connects using Windows Authentication that uses TCP/IP and has MultiSubnetFailover enabled. + + .EXAMPLE + $server = Connect-DbaInstance sql2016 -ApplicationIntent ReadOnly + + Connects with ReadOnly ApplicationIntent. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("SqlCredential")] + [PSCredential]$Credential, + [object[]]$Database, + [string]$AccessToken, + [ValidateSet('ReadOnly', 'ReadWrite')] + [string]$ApplicationIntent, + [string]$BatchSeparator, + [string]$ClientName = "dbatools PowerShell module - dbatools.io - custom connection", + [int]$ConnectTimeout = ([Sqlcollaborative.Dbatools.Connection.ConnectionHost]::SqlConnectionTimeout), + [switch]$EncryptConnection, + [string]$FailoverPartner, + [switch]$IsActiveDirectoryUniversalAuth, + [int]$LockTimeout, + [int]$MaxPoolSize, + [int]$MinPoolSize, + [switch]$MultipleActiveResultSets, + [switch]$MultiSubnetFailover, + [ValidateSet('TcpIp', 'NamedPipes', 'Multiprotocol', 'AppleTalk', 'BanyanVines', 'Via', 'SharedMemory', 'NWLinkIpxSpx')] + [string]$NetworkProtocol, + [switch]$NonPooledConnection, + [int]$PacketSize, + [int]$PooledConnectionLifetime, + [ValidateSet('CaptureSql', 'ExecuteAndCaptureSql', 'ExecuteSql')] + [string]$SqlExecutionModes, + [int]$StatementTimeout, + [switch]$TrustServerCertificate, + [string]$WorkstationId, + [string]$AppendConnectionString, + [switch]$SqlConnectionOnly + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Connect-DbaSqlServer + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-DbaInstance + + $loadedSmoVersion = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.Fullname -like "Microsoft.SqlServer.SMO,*" } + + if ($loadedSmoVersion) { + $loadedSmoVersion = $loadedSmoVersion | ForEach-Object { + if ($_.Location -match "__") { + ((Split-Path (Split-Path $_.Location) -Leaf) -split "__")[0] + } + else { + ((Get-ChildItem -Path $_.Location).VersionInfo.ProductVersion) + } + } + } + #'PrimaryFilePath' seems the culprit for slow SMO on databases + $Fields2000_Db = 'Collation', 'CompatibilityLevel', 'CreateDate', 'ID', 'IsAccessible', 'IsFullTextEnabled', 'IsSystemObject', 'IsUpdateable', 'LastBackupDate', 'LastDifferentialBackupDate', 'LastLogBackupDate', 'Name', 'Owner', 'ReadOnly', 'RecoveryModel', 'ReplicationOptions', 'Status', 'Version' + $Fields200x_Db = $Fields2000_Db + @('BrokerEnabled', 'IsMirroringEnabled', 'Trustworthy') + $Fields201x_Db = $Fields200x_Db + @('ActiveConnections', 'AvailabilityDatabaseSynchronizationState', 'AvailabilityGroupName', 'ContainmentType', 'EncryptionEnabled') + + $Fields2000_Login = 'CreateDate' , 'DateLastModified' , 'DefaultDatabase' , 'DenyWindowsLogin' , 'IsSystemObject' , 'Language' , 'LanguageAlias' , 'LoginType' , 'Name' , 'Sid' , 'WindowsLoginAccessType' + $Fields200x_Login = $Fields2000_Login + @('AsymmetricKey', 'Certificate', 'Credential', 'ID', 'IsDisabled', 'IsLocked', 'IsPasswordExpired', 'MustChangePassword', 'PasswordExpirationEnabled', 'PasswordPolicyEnforced') + $Fields201x_Login = $Fields200x_Login + @('PasswordHashAlgorithm') + + + } + process { + foreach ($instance in $SqlInstance) { + if ($instance.Type -like "Server") { + if ($instance.InputObject.ConnectionContext.IsOpen -eq $false) { + $instance.InputObject.ConnectionContext.Connect() + } + if ($SqlConnectionOnly) { return $instance.InputObject.ConnectionContext.SqlConnectionObject } + else { return $instance.InputObject } + } + if ($instance.Type -like "SqlConnection") { + $server = New-Object Microsoft.SqlServer.Management.Smo.Server($instance.InputObject) + + if ($server.ConnectionContext.IsOpen -eq $false) { + $server.ConnectionContext.Connect() + } + if ($SqlConnectionOnly) { return $server.ConnectionContext.SqlConnectionObject } + else { return $server } + } + + if ($instance.IsConnectionString) { $server = New-Object Microsoft.SqlServer.Management.Smo.Server($instance.InputObject) } + else { $server = New-Object Microsoft.SqlServer.Management.Smo.Server $instance.FullSmoName } + + if ($AppendConnectionString) { + $connstring = $server.ConnectionContext.ConnectionString + $server.ConnectionContext.ConnectionString = "$connstring;$appendconnectionstring" + $server.ConnectionContext.Connect() + } + else { + + $server.ConnectionContext.ApplicationName = $ClientName + + if (Test-Bound -ParameterName 'AccessToken') { $server.ConnectionContext.AccessToken = $AccessToken } + if (Test-Bound -ParameterName 'BatchSeparator') { $server.ConnectionContext.BatchSeparator = $BatchSeparator } + if (Test-Bound -ParameterName 'ConnectTimeout') { $server.ConnectionContext.ConnectTimeout = $ConnectTimeout } + if (Test-Bound -ParameterName 'Database') { $server.ConnectionContext.DatabaseName = $Database } + if (Test-Bound -ParameterName 'EncryptConnection') { $server.ConnectionContext.EncryptConnection = $true } + if (Test-Bound -ParameterName 'IsActiveDirectoryUniversalAuth') { $server.ConnectionContext.IsActiveDirectoryUniversalAuth = $true } + if (Test-Bound -ParameterName 'LockTimeout') { $server.ConnectionContext.LockTimeout = $LockTimeout } + if (Test-Bound -ParameterName 'MaxPoolSize') { $server.ConnectionContext.MaxPoolSize = $MaxPoolSize } + if (Test-Bound -ParameterName 'MinPoolSize') { $server.ConnectionContext.MinPoolSize = $MinPoolSize } + if (Test-Bound -ParameterName 'MultipleActiveResultSets') { $server.ConnectionContext.MultipleActiveResultSets = $true } + if (Test-Bound -ParameterName 'NetworkProtocol') { $server.ConnectionContext.NetworkProtocol = $NetworkProtocol } + if (Test-Bound -ParameterName 'NonPooledConnection') { $server.ConnectionContext.NonPooledConnection = $true } + if (Test-Bound -ParameterName 'PacketSize') { $server.ConnectionContext.PacketSize = $PacketSize } + if (Test-Bound -ParameterName 'PooledConnectionLifetime') { $server.ConnectionContext.PooledConnectionLifetime = $PooledConnectionLifetime } + if (Test-Bound -ParameterName 'StatementTimeout') { $server.ConnectionContext.StatementTimeout = $StatementTimeout } + if (Test-Bound -ParameterName 'SqlExecutionModes') { $server.ConnectionContext.SqlExecutionModes = $SqlExecutionModes } + if (Test-Bound -ParameterName 'TrustServerCertificate') { $server.ConnectionContext.TrustServerCertificate = $true } + if (Test-Bound -ParameterName 'WorkstationId') { $server.ConnectionContext.WorkstationId = $WorkstationId } + + $connstring = $server.ConnectionContext.ConnectionString + if (Test-Bound -ParameterName 'MultiSubnetFailover') { $connstring = "$connstring;MultiSubnetFailover=True" } + if (Test-Bound -ParameterName 'FailoverPartner') { $connstring = "$connstring;Failover Partner=$FailoverPartner" } + if (Test-Bound -ParameterName 'ApplicationIntent') { $connstring = "$connstring;ApplicationIntent=$ApplicationIntent" } + + if ($connstring -ne $server.ConnectionContext.ConnectionString) { + $server.ConnectionContext.ConnectionString = $connstring + } + + try { + if ($null -ne $Credential.username) { + $username = ($Credential.username).TrimStart("\") + + if ($username -like "*\*") { + $username = $username.Split("\")[1] + $authtype = "Windows Authentication with Credential" + $server.ConnectionContext.LoginSecure = $true + $server.ConnectionContext.ConnectAsUser = $true + $server.ConnectionContext.ConnectAsUserName = $username + $server.ConnectionContext.ConnectAsUserPassword = ($Credential).GetNetworkCredential().Password + } + else { + $authtype = "SQL Authentication" + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($username) + $server.ConnectionContext.set_SecurePassword($Credential.Password) + } + } + + $server.ConnectionContext.Connect() + } + catch { + $message = $_.Exception.InnerException.InnerException + $message = $message.ToString() + $message = ($message -Split '-->')[0] + $message = ($message -Split 'at System.Data.SqlClient')[0] + $message = ($message -Split 'at System.Data.ProviderBase')[0] + throw "Can't connect to $instance`: $message " + } + + } + + if ($loadedSmoVersion -ge 11) { + if ($server.VersionMajor -eq 8) { + # 2000 + $initFieldsDb = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsDb.AddRange($Fields2000_Db) + $initFieldsLogin = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsLogin.AddRange($Fields2000_Login) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Database], $initFieldsDb) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Login], $initFieldsLogin) + } + + elseif ($server.VersionMajor -eq 9 -or $server.VersionMajor -eq 10) { + # 2005 and 2008 + $initFieldsDb = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsDb.AddRange($Fields200x_Db) + $initFieldsLogin = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsLogin.AddRange($Fields200x_Login) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Database], $initFieldsDb) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Login], $initFieldsLogin) + } + + else { + # 2012 and above + $initFieldsDb = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsDb.AddRange($Fields201x_Db) + $initFieldsLogin = New-Object System.Collections.Specialized.StringCollection + [void]$initFieldsLogin.AddRange($Fields201x_Login) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Database], $initFieldsDb) + $server.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Login], $initFieldsLogin) + } + } + + if ($SqlConnectionOnly) { return $server.ConnectionContext.SqlConnectionObject } + else { return $server } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function ConvertTo-DbaDataTable { + <# + .SYNOPSIS + Creates a DataTable for an object. + + .DESCRIPTION + Creates a DataTable based on an object's properties. This allows you to easily write to SQL Server tables. + + Thanks to Chad Miller, this is based on his script. https://gallery.technet.microsoft.com/scriptcenter/4208a159-a52e-4b99-83d4-8048468d29dd + + If the attempt to convert to datatable fails, try the -Raw parameter for less accurate datatype detection. + + .PARAMETER InputObject + The object to transform into a DataTable. + + .PARAMETER TimeSpanType + Specifies the type to convert TimeSpan objects into. Default is 'TotalMilliseconds'. Valid options are: 'Ticks', 'TotalDays', 'TotalHours', 'TotalMinutes', 'TotalSeconds', 'TotalMilliseconds', and 'String'. + + .PARAMETER SizeType + Specifies the type to convert DbaSize objects to. Default is 'Int64'. Valid options are 'Int32', 'Int64', and 'String'. + + .PARAMETER IgnoreNull + If this switch is enabled, objects with null values will be ignored (empty rows will be added by default). + + .PARAMETER Raw + If this switch is enabled, the DataTable will be created with strings. No attempt will be made to parse/determine data types. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DataTable, Table, Data + Website: https://dbatools.io/ + Copyright: (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/ConvertTo-DbaDataTable + + .OUTPUTS + System.Object[] + + .EXAMPLE + Get-Service | ConvertTo-DbaDataTable + + Creates a DataTable from the output of Get-Service. + + .EXAMPLE + ConvertTo-DbaDataTable -InputObject $csv.cheesetypes + + Creates a DataTable from the CSV object $csv.cheesetypes. + + .EXAMPLE + $dblist | ConvertTo-DbaDataTable + + Creates a DataTable from the $dblist object passed in via pipeline. + + .EXAMPLE + Get-Process | ConvertTo-DbaDataTable -TimeSpanType TotalSeconds + + Creates a DataTable with the running processes and converts any TimeSpan property to TotalSeconds. + #> + [CmdletBinding()] + [OutputType([System.Object[]])] + param ( + [Parameter(Position = 0, + Mandatory = $true, + ValueFromPipeline = $true)] + [AllowNull()] + [PSObject[]]$InputObject, + [Parameter(Position = 1)] + [ValidateSet("Ticks", + "TotalDays", + "TotalHours", + "TotalMinutes", + "TotalSeconds", + "TotalMilliseconds", + "String")] + [ValidateNotNullOrEmpty()] + [string]$TimeSpanType = "TotalMilliseconds", + [ValidateSet("Int64", "Int32", "String")] + [string]$SizeType = "Int64", + [switch]$IgnoreNull, + [switch]$Raw, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Write-Message -Level Debug -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + Write-Message -Level Debug -Message "TimeSpanType = $TimeSpanType | SizeType = $SizeType" + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Alias Out-DbaDataTable + + function Convert-Type { + # This function will check so that the type is an accepted type which could be used when inserting into a table. + # If a type is accepted (included in the $type array) then it will be passed on, otherwise it will first change type before passing it on. + # Special types will have both their types converted as well as the value. + # TimeSpan is a special type and will be converted into the $timespantype. (default: TotalMilliseconds) so that the timespan can be stored in a database further down the line. + [CmdletBinding()] + param ( + $type, + + $value, + + $timespantype = 'TotalMilliseconds', + + $sizetype = 'Int64' + ) + + $types = [System.Collections.ArrayList]@( + 'System.Int32', + 'System.UInt32', + 'System.Int16', + 'System.UInt16', + 'System.Int64', + 'System.UInt64', + 'System.Decimal', + 'System.Single', + 'System.Double', + 'System.Byte', + 'System.SByte', + 'System.Boolean', + 'System.DateTime', + 'System.Guid', + 'System.Char' + ) + + # The $special variable is used to mark the return value if a conversion was made on the value itself. + # If this is set to true the original value will later be ignored when updating the DataTable. + # And the value returned from this function will be used instead. (cannot modify existing properties) + $special = $false + $specialType = "" + + # Special types need to be converted in some way. + # This attempt is to convert timespan into something that works in a table. + # I couldn't decide on what to convert it to so the user can decide. + # If the parameter is not used, TotalMilliseconds will be used as default. + # Ticks are more accurate but I think milliseconds are more useful most of the time. + if (($type -eq 'System.TimeSpan') -or ($type -eq 'Sqlcollaborative.Dbatools.Utility.DbaTimeSpan') -or ($type -eq 'Sqlcollaborative.Dbatools.Utility.DbaTimeSpanPretty')) { + $special = $true + if ($timespantype -eq 'String') { + $value = $value.ToString() + $type = 'System.String' + } + else { + # Let's use Int64 for all other types than string. + # We could match the type more closely with the timespantype but that can be added in the future if needed. + $value = $value.$timespantype + $type = 'System.Int64' + } + $specialType = 'Timespan' + } + elseif ($type -eq 'Sqlcollaborative.Dbatools.Utility.Size') { + $special = $true + switch ($sizetype) { + 'Int64' { + $value = $value.Byte + $type = 'System.Int64' + } + 'Int32' { + $value = $value.Byte + $type = 'System.Int32' + } + 'String' { + $value = $value.ToString() + $type = 'System.String' + } + } + $specialType = 'Size' + } + elseif (-not ($type -in $types)) { + # All types which are not found in the array will be converted into strings. + # In this way we dont ignore it completely and it will be clear in the end why it looks as it does. + $type = 'System.String' + } + + # return a hashtable instead of an object. I like hashtables :) + return @{ type = $type; Value = $value; Special = $special; SpecialType = $specialType } + } + + function Convert-SpecialType { + <# + .SYNOPSIS + Converts a value for a known column. + + .DESCRIPTION + Converts a value for a known column. + + .PARAMETER Value + The value to convert + + .PARAMETER Type + The special type for which to convert + + .PARAMETER SizeType + The size type defined by the user + + .PARAMETER TimeSpanType + The timespan type defined by the user + #> + [CmdletBinding()] + Param ( + $Value, + [ValidateSet('Timespan', 'Size')] [string]$Type, + [string]$SizeType, + [string]$TimeSpanType + ) + + switch ($Type) { + 'Size' { + if ($SizeType -eq 'String') { return $Value.ToString() } + else { return $Value.Byte } + } + 'Timespan' { + if ($TimeSpanType -eq 'String') { + $Value.ToString() + } + else { + $Value.$TimeSpanType + } + } + } + } + + function Add-Column { + <# + .SYNOPSIS + Adds a column to the datatable in progress. + + .DESCRIPTION + Adds a column to the datatable in progress. + + .PARAMETER Property + The property for which to add a column. + + .PARAMETER DataTable + Autofilled. The table for which to add a column. + + .PARAMETER TimeSpanType + Autofilled. How should timespans be handled? + + .PARAMETER SizeType + Autofilled. How should sizes be handled? + + .PARAMETER Raw + Autofilled. Whether the column should be string, no matter the input. + #> + [CmdletBinding()] + Param ( + [System.Management.Automation.PSPropertyInfo]$Property, + [System.Data.DataTable]$DataTable = $datatable, + [string]$TimeSpanType = $TimeSpanType, + [string]$SizeType = $SizeType, + [bool]$Raw = $Raw + ) + + $type = $property.TypeNameOfValue + try { + if ($Property.MemberType -like 'ScriptProperty') { + $type = $Property.GetType().FullName + } + } + catch { $type = 'System.String' } + + $converted = Convert-Type -type $type -value $property.Value -timespantype $TimeSpanType -sizetype $SizeType + + $column = New-Object System.Data.DataColumn + $column.ColumnName = $property.Name.ToString() + if (-not $Raw) { + $column.DataType = [System.Type]::GetType($converted.type) + } + $null = $DataTable.Columns.Add($column) + $converted + } + + $datatable = New-Object System.Data.DataTable + + # Accelerate subsequent lookups of columns and special type columns + $columns = @() + $specialColumns = @() + $specialColumnsType = @{ } + + $ShouldCreateColumns = $true + } + + process { + #region Handle null objects + if ($null -eq $InputObject) { + if (-not $IgnoreNull) { + $datarow = $datatable.NewRow() + $datatable.Rows.Add($datarow) + } + + # Only ends the current process block + return + } + #endregion Handle null objects + + + foreach ($object in $InputObject) { + #region Handle null objects + if ($null -eq $object) { + if (-not $IgnoreNull) { + $datarow = $datatable.NewRow() + $datatable.Rows.Add($datarow) + } + continue + } + #endregion Handle null objects + + #Handle rows already being System.Data.DataRow + if ($object.GetType().FullName -eq 'System.Data.DataRow') { + if ($ShouldCreateColumns) { + $datatable = $object.Table.Copy() + $ShouldCreateColumns = $false + } + continue + } + + # The new row to insert + $datarow = $datatable.NewRow() + + #region Process Properties + $objectProperties = $object.PSObject.Properties + foreach ($property in $objectProperties) { + #region Create Columns as needed + if ($ShouldCreateColumns) { + $newColumn = Add-Column -Property $property + $columns += $property.Name + if ($newColumn.Special) { + $specialColumns += $property.Name + $specialColumnsType[$property.Name] = $newColumn.SpecialType + } + } + #endregion Create Columns as needed + + # Handle null properties, as well as properties with access errors + try { + $propValueLength = $property.value.length + } + catch { + $propValueLength = 0 + } + + #region Insert value into column of row + if ($propValueLength -gt 0) { + # If the typename was a special typename we want to use the value returned from Convert-Type instead. + # We might get error if we try to change the value for $property.value if it is read-only. That's why we use $converted.value instead. + if ($property.Name -in $specialColumns) { + $datarow.Item($property.Name) = Convert-SpecialType -Value $property.value -Type $specialColumnsType[$property.Name] -SizeType $SizeType -TimeSpanType $TimeSpanType + } + else { + if ($property.value.ToString().length -eq 15) { + if ($property.value.ToString() -eq 'System.Object[]') { + $value = $property.value -join ", " + } + elseif ($property.value.ToString() -eq 'System.String[]') { + $value = $property.value -join ", " + } + else { + $value = $property.value + } + } + else { + $value = $property.value + } + + try { + $datarow.Item($property.Name) = $value + } + catch { + if ($property.Name -notin $columns) { + try { + $newColumn = Add-Column -Property $property + $columns += $property.Name + if ($newColumn.Special) { + $specialColumns += $property.Name + $specialColumnsType[$property.Name] = $newColumn.SpecialType + } + + $datarow.Item($property.Name) = $newColumn.Value + } + catch { + Write-Message -Level Warning -Message "Failed to add property $($property.Name) from $object" -ErrorRecord $_ -Target $object + } + } + else { + Write-Message -Level Warning -Message "Failed to add property $($property.Name) from $object" -ErrorRecord $_ -Target $object + } + } + } + } + #endregion Insert value into column of row + } + + $datatable.Rows.Add($datarow) + # If this is the first non-null object then the columns has just been created. + # Set variable to false to skip creating columns from now on. + if ($ShouldCreateColumns) { + $ShouldCreateColumns = $false + } + #endregion Process Properties + } + } + + end { + Write-Message -Level InternalComment -Message "Finished." + , $datatable + } +} +function ConvertTo-DbaXESession { + <# + .SYNOPSIS + Uses a slightly modified version of sp_SQLskills_ConvertTraceToExtendedEvents.sql to convert Traces to Extended Events. + + .DESCRIPTION + Uses a slightly modified version of sp_SQLskills_ConvertTraceToExtendedEvents.sql to convert Traces to Extended Events. + + T-SQL code by: Jonathan M. Kehayias, SQLskills.com. T-SQL can be found in this module directory and at + https://www.sqlskills.com/blogs/jonathan/converting-sql-trace-to-extended-events-in-sql-server-2012/ + + .PARAMETER InputObject + Specifies a Trace object output by Get-DbaTrace. + + .PARAMETER Name + The name of the Trace to convert. If the name exists, characters will be appended to it. + + .PARAMETER OutputScriptOnly + Outputs the T-SQL script to create the XE session and does not execute it. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Trace, ExtendedEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2017, sql2012 | Where Id -eq 2 | ConvertTo-DbaXESession -Name 'Test' + + Converts Trace with ID 2 to a Session named Test on SQL Server instances named sql2017 and sql2012 + and creates the Session on each respective server. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2014 | Out-GridView -PassThru | ConvertTo-DbaXESession -Name 'Test' | Start-DbaXESession + + Converts selected traces on sql2014 to sessions, creates the session, and starts it. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2014 | Where Id -eq 1 | ConvertTo-DbaXESession -Name 'Test' -OutputScriptOnly + + Converts trace ID 1 on sql2014 to an Extended Event and outputs the resulting T-SQL. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [object[]]$InputObject, + [parameter(Mandatory)] + [string]$Name, + [switch]$OutputScriptOnly, + [switch]$EnableException + ) + begin { + $rawsql = Get-Content "$script:PSModuleRoot\bin\sp_SQLskills_ConvertTraceToEEs.sql" -Raw + } + process { + foreach ($trace in $InputObject) { + if (-not $trace.id -and -not $trace.Parent) { + Stop-Function -Message "Input is of the wrong type. Use Get-DbaTrace." -Continue + return + } + + $server = $trace.Parent + + if ($server.VersionMajor -lt 11) { + Stop-Function -Message "SQL Server version 2012+ required - $server not supported." + return + } + + $tempdb = $server.Databases['tempdb'] + $traceid = $trace.id + + if ((Get-DbaXESession -SqlInstance $server -Session $PSBoundParameters.Name)) { + $oldname = $name + $Name = "$name-$traceid" + Write-Message -Level Output -Message "XE Session $oldname already exists on $server, trying $name." + } + + if ((Get-DbaXESession -SqlInstance $server -Session $Name)) { + $oldname = $name + $Name = "$name-$(Get-Random)" + Write-Message -Level Output -Message "XE Session $oldname already exists on $server, trying $name." + } + + $sql = $rawsql.Replace("--TRACEID--", $traceid) + $sql = $sql.Replace("--SESSIONNAME--", $name) + + try { + Write-Message -Level Verbose -Message "Executing SQL in tempdb." + $results = $tempdb.ExecuteWithResults($sql).Tables.Rows.SqlString + } + catch { + Stop-Function -Message "Issue creating, dropping or executing sp_SQLskills_ConvertTraceToExtendedEvents in tempdb on $server." -Target $server -ErrorRecord $_ + } + + $results = $results -join "`r`n" + + if ($OutputScriptOnly) { + $results + } + else { + Write-Message -Level Verbose -Message "Creating XE Session $name." + try { + $tempdb.ExecuteNonQuery($results) + } + catch { + Stop-Function -Message "Issue creating extended event $name on $server." -Target $server -ErrorRecord $_ + } + Get-DbaXESession -SqlInstance $server -Session $name + } + } + } +} +function Copy-DbaAgentAlert { + <# + .SYNOPSIS + Copy-DbaAgentAlert migrates alerts from one SQL Server to another. + + .DESCRIPTION + By default, all alerts are copied. The -Alert parameter is auto-populated for command-line completion and can be used to copy only specific alerts. + + If the alert already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Alert + The alert(s) to process. This list is auto-populated from the server. If unspecified, all alerts will be processed. + + .PARAMETER ExcludeAlert + The alert(s) to exclude. This list is auto-populated from the server. + + .PARAMETER IncludeDefaults + Copy SQL Agent defaults such as FailSafeEmailAddress, ForwardingServer, and PagerSubjectTemplate. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Alert will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentAlert + + .EXAMPLE + Copy-DbaAgentAlert -Source sqlserver2014a -Destination sqlcluster + + Copies all alerts from sqlserver2014a to sqlcluster using Windows credentials. If alerts with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentAlert -Source sqlserver2014a -Destination sqlcluster -Alert PSAlert -SourceSqlCredential $cred -Force + + Copies a only the alert named PSAlert from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a alert with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaAgentAlert -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [cmdletbinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Alert, + [object[]]$ExcludeAlert, + [switch]$IncludeDefaults, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $Destination = $destServer.DomainInstanceName + + } + process { + + $serverAlerts = $sourceServer.JobServer.Alerts + $destAlerts = $destServer.JobServer.Alerts + + if ($IncludeDefaults -eq $true) { + if ($PSCmdlet.ShouldProcess($Destination, "Creating Alert Defaults")) { + $copyAgentAlertStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = "Alert Defaults" + Type = "Alert Defaults" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + try { + Write-Message -Message "Creating Alert Defaults" -Level Verbose + $sql = $sourceServer.JobServer.AlertSystem.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$Destination'" + + Write-Message -Message $sql -Level Debug + $null = $destServer.Query($sql) + + $copyAgentAlertStatus.Status = "Successful" + } + catch { + $copyAgentAlertStatus.Status = "Failed" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue creating alert defaults." -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer -Continue + } + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + foreach ($serverAlert in $serverAlerts) { + $alertName = $serverAlert.name + $copyAgentAlertStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $alertName + Type = "Agent Alert" + Notes = $null + Status = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + if (($Alert -and $Alert -notcontains $alertName) -or ($ExcludeAlert -and $ExcludeAlert -contains $alertName)) { + continue + } + + if ($destAlerts.name -contains $serverAlert.name) { + if ($force -eq $false) { + $copyAgentAlertStatus.Status = "Skipped" + $copyAgentAlertStatus.Notes = "Already exists" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Message "Alert [$alertName] exists at destination. Use -Force to drop and migrate." -Level Verbose + continue + } + + if ($PSCmdlet.ShouldProcess($Destination, "Dropping alert $alertName and recreating")) { + try { + Write-Message -Message "Dropping Alert $alertName on $destServer." -Level Verbose + + $sql = "EXEC msdb.dbo.sp_delete_alert @name = N'$($alertname)';" + Write-Message -Message $sql -Level Debug + $null = $destServer.Query($sql) + $destAlerts.Refresh() + } + catch { + $copyAgentAlertStatus.Status = "Failed" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping/recreating alert" -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer -Continue + } + } + } + + if ($destAlerts | Where-Object { $_.Severity -eq $serverAlert.Severity -and $_.MessageID -eq $serverAlert.MessageID -and $_.DatabaseName -eq $serverAlert.DatabaseName -and $_.EventDescriptionKeyword -eq $serverAlert.EventDescriptionKeyword }) { + $conflictMessage = "Alert [$alertName] has already been defined to use" + if ($serverAlert.Severity -gt 0) { $conflictMessage += " severity $($serverAlert.Severity)" } + if ($serverAlert.MessageID -gt 0) { $conflictMessage += " error number $($serverAlert.MessageID)" } + if ($serverAlert.DatabaseName) { $conflictMessage += " on database '$($serverAlert.DatabaseName)'" } + if ($serverAlert.EventDescriptionKeyword) { $conflictMessage += " with error text '$($serverAlert.Severity)'" } + $conflictMessage += ". Skipping." + + Write-Message -Level Verbose -Message $conflictMessage + $copyAgentAlertStatus.Status = "Skipped" + $copyAgentAlertStatus.Notes = $conflictMessage + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + if ($serverAlert.JobName -and $destServer.JobServer.Jobs.Name -NotContains $serverAlert.JobName) { + Write-Message -Level Verbose -Message "Alert [$alertName] has job [$($serverAlert.JobName)] configured as response. The job does not exist on destination $destServer. Skipping." + + $copyAgentAlertStatus.Status = "Skipped" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($PSCmdlet.ShouldProcess($Destination, "Creating Alert $alertName")) { + try { + Write-Message -Message "Copying Alert $alertName" -Level Verbose + $sql = $serverAlert.Script() | Out-String + $sql = $sql -replace "@job_id=N'........-....-....-....-............", "@job_id=N'00000000-0000-0000-0000-000000000000" + + Write-Message -Message $sql -Level Debug + $null = $destServer.Query($sql) + + $copyAgentAlertStatus.Status = "Successful" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyAgentAlertStatus.Status = "Failed" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue creating alert" -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer -Continue + } + } + + $destServer.JobServer.Alerts.Refresh() + $destServer.JobServer.Jobs.Refresh() + + $newAlert = $destServer.JobServer.Alerts[$alertName] + $notifications = $serverAlert.EnumNotifications() + $jobName = $serverAlert.JobName + + # JobId = 00000000-0000-0000-0000-000 means the Alert does not execute/is attached to a SQL Agent Job. + if ($serverAlert.JobId -ne '00000000-0000-0000-0000-000000000000') { + $copyAgentAlertStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $alertName + Type = "Agent Alert Job Association" + Notes = "Associated with $jobName" + Status = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + if ($PSCmdlet.ShouldProcess($Destination, "Adding $alertName to $jobName")) { + try { + <# THERE needs to be validation within this block to see if the $jobName actually exists on the source server. #> + Write-Message -Message "Adding $alertName to $jobName" -Level Verbose + $newJob = $destServer.JobServer.Jobs[$jobName] + $newJobId = ($newJob.JobId) -replace " ", "" + $sql = $sql -replace '00000000-0000-0000-0000-000000000000', $newJobId + $sql = $sql -replace 'sp_add_alert', 'sp_update_alert' + + Write-Message -Message $sql -Level Debug + $null = $destServer.Query($sql) + + $copyAgentAlertStatus.Status = "Successful" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyAgentAlertStatus.Status = "Failed" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue adding alert to job" -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer + } + } + } + + if ($PSCmdlet.ShouldProcess($Destination, "Moving Notifications $alertName")) { + try { + $copyAgentAlertStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $alertName + Type = "Agent Alert Notification" + Notes = $null + Status = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + # cant add them this way, we need to modify the existing one or give all options that are supported. + foreach ($notify in $notifications) { + $notifyCollection = @() + if ($notify.UseNetSend -eq $true) { + Write-Message -Message "Adding net send" -Level Verbose + $notifyCollection += "NetSend" + } + + if ($notify.UseEmail -eq $true) { + Write-Message -Message "Adding email" -Level Verbose + $notifyCollection += "NotifyEmail" + } + + if ($notify.UsePager -eq $true) { + Write-Message -Message "Adding pager" -Level Verbose + $notifyCollection += "Pager" + } + + $notifyMethods = $notifyCollection -join ", " + $newAlert.AddNotification($notify.OperatorName, [Microsoft.SqlServer.Management.Smo.Agent.NotifyMethods]$notifyMethods) + } + $copyAgentAlertStatus.Status = "Successful" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyAgentAlertStatus.Status = "Failed" + $copyAgentAlertStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue moving notifications for the alert" -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlAlert + } +} +#ValidationTags#Messaging# +function Copy-DbaAgentCategory { + <# + .SYNOPSIS + Copy-DbaAgentCategory migrates SQL Agent categories from one SQL Server to another. This is similar to sp_add_category. + + https://msdn.microsoft.com/en-us/library/ms181597.aspx + + .DESCRIPTION + By default, all SQL Agent categories for Jobs, Operators and Alerts are copied. + + The -OperatorCategories parameter is auto-populated for command-line completion and can be used to copy only specific operator categories. + The -AgentCategories parameter is auto-populated for command-line completion and can be used to copy only specific agent categories. + The -JobCategories parameter is auto-populated for command-line completion and can be used to copy only specific job categories. + + If the category already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER CategoryType + Specifies the Category Type to migrate. Valid options are "Job", "Alert" and "Operator". When CategoryType is specified, all categories from the selected type will be migrated. For granular migrations, use the three parameters below. + + .PARAMETER OperatorCategory + This parameter is auto-populated for command-line completion and can be used to copy only specific operator categories. + + .PARAMETER AgentCategory + This parameter is auto-populated for command-line completion and can be used to copy only specific agent categories. + + .PARAMETER JobCategory + This parameter is auto-populated for command-line completion and can be used to copy only specific job categories. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Category will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentCategory + + .EXAMPLE + Copy-DbaAgentCategory -Source sqlserver2014a -Destination sqlcluster + + Copies all operator categories from sqlserver2014a to sqlcluster using Windows authentication. If operator categories with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentCategory -Source sqlserver2014a -Destination sqlcluster -OperatorCategory PSOperator -SourceSqlCredential $cred -Force + + Copies a single operator category, the PSOperator operator category from sqlserver2014a to sqlcluster using SQL credentials to authenticate to sqlserver2014a and Windows credentials for sqlcluster. If a operator category with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaAgentCategory -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldprocess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [Parameter(ParameterSetName = 'SpecificAlerts')] + [ValidateSet('Job', 'Alert', 'Operator')] + [string[]]$CategoryType, + [string[]]$JobCategory, + [string[]]$AgentCategory, + [string[]]$OperatorCategory, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Copy-JobCategory { + <# + .SYNOPSIS + Copy-JobCategory migrates job categories from one SQL Server to another. + + .DESCRIPTION + By default, all job categories are copied. The -JobCategories parameter is auto-populated for command-line completion and can be used to copy only specific job categories. + + If the associated credential for the category does not exist on the destination, it will be skipped. If the job category already exists on the destination, it will be skipped unless -Force is used. + #> + param ( + [string[]]$jobCategories + ) + + process { + + $serverJobCategories = $sourceServer.JobServer.JobCategories | Where-Object ID -ge 100 + $destJobCategories = $destServer.JobServer.JobCategories | Where-Object ID -ge 100 + + foreach ($jobCategory in $serverJobCategories) { + $categoryName = $jobCategory.Name + + $copyJobCategoryStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $categoryName + Type = "Agent Job Category" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($jobCategories.Count -gt 0 -and $jobCategories -notcontains $categoryName) { + continue + } + + if ($destJobCategories.Name -contains $jobCategory.name) { + if ($force -eq $false) { + $copyJobCategoryStatus.Status = "Skipped" + $copyJobCategoryStatus.Notes = "Already exists" + $copyJobCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Job category $categoryName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping job category $categoryName")) { + try { + Write-Message -Level Verbose -Message "Dropping Job category $categoryName" + $destServer.JobServer.JobCategories[$categoryName].Drop() + } + catch { + $copyJobCategoryStatus.Status = "Failed" + $copyJobCategoryStatus + Stop-Function -Message "Issue dropping job category" -Target $categoryName -InnerErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating Job category $categoryName")) { + try { + Write-Message -Level Verbose -Message "Copying Job category $categoryName" + $sql = $jobCategory.Script() | Out-String + Write-Message -Level Debug -Message "SQL Statement: $sql" + $destServer.Query($sql) + + $copyJobCategoryStatus.Status = "Successful" + $copyJobCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyJobCategoryStatus.Status = "Failed" + $copyJobCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying job category" -Target $categoryName -InnerErrorRecord $_ + } + } + } + } + } + + function Copy-OperatorCategory { + <# + .SYNOPSIS + Copy-OperatorCategory migrates operator categories from one SQL Server to another. + + .DESCRIPTION + By default, all operator categories are copied. The -OperatorCategories parameter is auto-populated for command-line completion and can be used to copy only specific operator categories. + + If the associated credential for the category does not exist on the destination, it will be skipped. If the operator category already exists on the destination, it will be skipped unless -Force is used. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldprocess = $true)] + param ( + [string[]]$operatorCategories + ) + process { + $serverOperatorCategories = $sourceServer.JobServer.OperatorCategories | Where-Object ID -ge 100 + $destOperatorCategories = $destServer.JobServer.OperatorCategories | Where-Object ID -ge 100 + + foreach ($operatorCategory in $serverOperatorCategories) { + $categoryName = $operatorCategory.Name + + $copyOperatorCategoryStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Agent Operator Category" + Name = $categoryName + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($operatorCategories.Count -gt 0 -and $operatorCategories -notcontains $categoryName) { + continue + } + + if ($destOperatorCategories.Name -contains $operatorCategory.Name) { + if ($force -eq $false) { + $copyOperatorCategoryStatus.Status = "Skipped" + $copyOperatorCategoryStatus.Notes = "Already exists" + $copyOperatorCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Operator category $categoryName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping operator category $categoryName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping Operator category $categoryName" + $destServer.JobServer.OperatorCategories[$categoryName].Drop() + Write-Message -Level Verbose -Message "Copying Operator category $categoryName" + $sql = $operatorCategory.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + } + catch { + $copyOperatorCategoryStatus.Status = "Failed" + $copyOperatorCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping operator category" -Target $categoryName -InnerErrorRecord $_ + } + } + } + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Creating Operator category $categoryName")) { + try { + Write-Message -Level Verbose -Message "Copying Operator category $categoryName" + $sql = $operatorCategory.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copyOperatorCategoryStatus.Status = "Successful" + $copyOperatorCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyOperatorCategoryStatus.Status = "Failed" + $copyOperatorCategoryStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying operator category" -Target $categoryName -InnerErrorRecord $_ + } + } + } + } + } + } + + function Copy-AlertCategory { + <# + .SYNOPSIS + Copy-AlertCategory migrates alert categories from one SQL Server to another. + + .DESCRIPTION + By default, all alert categories are copied. The -AlertCategories parameter is auto-populated for command-line completion and can be used to copy only specific alert categories. + + If the associated credential for the category does not exist on the destination, it will be skipped. If the alert category already exists on the destination, it will be skipped unless -Force is used. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldprocess = $true)] + param ( + [string[]]$AlertCategories + ) + + process { + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Server AlertCategories are only supported in SQL Server 2005 and above. Quitting." + } + + $serverAlertCategories = $sourceServer.JobServer.AlertCategories | Where-Object ID -ge 100 + $destAlertCategories = $destServer.JobServer.AlertCategories | Where-Object ID -ge 100 + + foreach ($alertCategory in $serverAlertCategories) { + $categoryName = $alertCategory.Name + + $copyAlertCategoryStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Agent Alert Category" + Name = $categoryName + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($alertCategories.Length -gt 0 -and $alertCategories -notcontains $categoryName) { + continue + } + + if ($destAlertCategories.Name -contains $alertCategory.name) { + if ($force -eq $false) { + $copyAlertCategoryStatus.Status = "Skipped" + $copyAlertCategoryStatus.Notes = "Already exists" + $copyAlertCategoryStatus + Write-Message -Level Verbose -Message "Alert category $categoryName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping alert category $categoryName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping Alert category $categoryName" + $destServer.JobServer.AlertCategories[$categoryName].Drop() + Write-Message -Level Verbose -Message "Copying Alert category $categoryName" + $sql = $alertcategory.Script() | Out-String + Write-Message -Level Debug -Message "SQL Statement: $sql" + $destServer.Query($sql) + } + catch { + $copyAlertCategoryStatus.Status = "Failed" + $copyAlertCategoryStatus + Stop-Function -Message "Issue dropping alert category" -Target $categoryName -InnerErrorRecord $_ + } + } + } + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Creating Alert category $categoryName")) { + try { + Write-Message -Level Verbose -Message "Copying Alert category $categoryName" + $sql = $alertCategory.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copyAlertCategoryStatus.Status = "Successful" + $copyAlertCategoryStatus + } + catch { + $copyAlertCategoryStatus.Status = "Failed" + $copyAlertCategoryStatus + Stop-Function -Message "Issue creating alert category" -Target $categoryName -InnerErrorRecord $_ + } + } + } + } + } + } + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + } + process { + if ($CategoryType.count -gt 0) { + + switch ($CategoryType) { + "Job" { + Copy-JobCategory + } + + "Alert" { + Copy-AlertCategory + } + + "Operator" { + Copy-OperatorCategory + } + } + + return + } + + if (($OperatorCategory.Count + $AlertCategory.Count + $jobCategory.Count) -gt 0) { + + if ($OperatorCategory.Count -gt 0) { + Copy-OperatorCategory -OperatorCategories $OperatorCategory + } + + if ($AlertCategory.Count -gt 0) { + Copy-AlertCategory -AlertCategories $AlertCategory + } + + if ($jobCategory.Count -gt 0) { + Copy-JobCategory -JobCategories $jobCategory + } + + return + } + + Copy-OperatorCategory + Copy-AlertCategory + Copy-JobCategory + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlAgentCategory + } +} +function Copy-DbaAgentJob { + <# + .SYNOPSIS + Copy-DbaAgentJob migrates jobs from one SQL Server to another. + + .DESCRIPTION + By default, all jobs are copied. The -Job parameter is auto-populated for command-line completion and can be used to copy only specific jobs. + + If the job already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The job(s) to process. This list is auto-populated from the server. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + The job(s) to exclude. This list is auto-populated from the server. + + .PARAMETER DisableOnSource + If this switch is enabled, the job will be disabled on the source server. + + .PARAMETER DisableOnDestination + If this switch is enabled, the newly migrated job will be disabled on the destination server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Job will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent, Job + Author: Chrissy LeMaire (@cl), netnerds.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentJob + + .EXAMPLE + Copy-DbaAgentJob -Source sqlserver2014a -Destination sqlcluster + + Copies all jobs from sqlserver2014a to sqlcluster, using Windows credentials. If jobs with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentJob -Source sqlserver2014a -Destination sqlcluster -Job PSJob -SourceSqlCredential $cred -Force + + Copies a single job, the PSJob job from sqlserver2014a to sqlcluster, using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a job with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaAgentJob -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [cmdletbinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Job, + [object[]]$ExcludeJob, + [switch]$DisableOnSource, + [switch]$DisableOnDestination, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + } + process { + + if (Test-FunctionInterrupt) { return } + + $serverJobs = $sourceServer.JobServer.Jobs + $destJobs = $destServer.JobServer.Jobs + + foreach ($serverJob in $serverJobs) { + $jobName = $serverJob.name + $jobId = $serverJob.JobId + + $copyJobStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $jobName + Type = "Agent Job" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Job -and $jobName -notin $Job -or $jobName -in $ExcludeJob) { + Write-Message -Level Verbose -Message "Job [$jobName] filtered. Skipping." + continue + } + Write-Message -Message "Working on job: $jobName" -Level Verbose + $sql = " + SELECT sp.[name] AS MaintenancePlanName + FROM msdb.dbo.sysmaintplan_plans AS sp + INNER JOIN msdb.dbo.sysmaintplan_subplans AS sps + ON sps.plan_id = sp.id + WHERE job_id = '$($jobId)'" + Write-Message -Message $sql -Level Debug + + $MaintenancePlanName = $sourceServer.Query($sql).MaintenancePlanName + + if ($MaintenancePlanName) { + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job is associated with maintenance plan" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Job [$jobName] is associated with Maintenance Plan: $MaintenancePlanName" + continue + } + + $dbNames = $serverJob.JobSteps.DatabaseName | Where-Object { $_.Length -gt 0 } + $missingDb = $dbNames | Where-Object { $destServer.Databases.Name -notcontains $_ } + + if ($missingDb.Count -gt 0 -and $dbNames.Count -gt 0) { + $missingDb = ($missingDb | Sort-Object | Get-Unique) -join ", " + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job is dependent on database: $missingDb" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Database(s) $missingDb doesn't exist on destination. Skipping job [$jobName]." + continue + } + + $missingLogin = $serverJob.OwnerLoginName | Where-Object { $destServer.Logins.Name -notcontains $_ } + + if ($missingLogin.Count -gt 0) { + if ($force -eq $false) { + $missingLogin = ($missingLogin | Sort-Object | Get-Unique) -join ", " + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job is dependent on login $missingLogin" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Login(s) $missingLogin doesn't exist on destination. Use -Force to set owner to [sa]. Skipping job [$jobName]." + continue + } + } + + $proxyNames = $serverJob.JobSteps.ProxyName | Where-Object { $_.Length -gt 0 } + $missingProxy = $proxyNames | Where-Object { $destServer.JobServer.ProxyAccounts.Name -notcontains $_ } + + if ($missingProxy.Count -gt 0 -and $proxyNames.Count -gt 0) { + $missingProxy = ($missingProxy | Sort-Object | Get-Unique) -join ", " + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job is dependent on proxy $($proxyNames[0])" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Proxy Account(s) $($proxyNames[0]) doesn't exist on destination. Skipping job [$jobName]." + continue + } + + $operators = $serverJob.OperatorToEmail, $serverJob.OperatorToNetSend, $serverJob.OperatorToPage | Where-Object { $_.Length -gt 0 } + $missingOperators = $operators | Where-Object {$destServer.JobServer.Operators.Name -notcontains $_} + + if ($missingOperators.Count -gt 0 -and $operators.Count -gt 0) { + $missingOperator = ($operators | Sort-Object | Get-Unique) -join ", " + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job is dependent on operator $missingOperator" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Operator(s) $($missingOperator) doesn't exist on destination. Skipping job [$jobName]" + continue + } + + if ($destJobs.name -contains $serverJob.name) { + if ($force -eq $false) { + $copyJobStatus.Status = "Skipped" + $copyJobStatus.Notes = "Job already exists on destination" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Job $jobName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping job $jobName and recreating")) { + try { + Write-Message -Message "Dropping Job $jobName" -Level Verbose + $destServer.JobServer.Jobs[$jobName].Drop() + } + catch { + $copyJobStatus.Status = "Failed" + $copyJobStatus.Notes = $_.Exception.Message + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping job" -Target $jobName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating Job $jobName")) { + try { + Write-Message -Message "Copying Job $jobName" -Level Verbose + $sql = $serverJob.Script() | Out-String + + if ($missingLogin.Count -gt 0 -and $force) { + $saLogin = Get-SqlSaLogin -SqlInstance $destServer + $sql = $sql -replace [Regex]::Escape("@owner_login_name=N'$missingLogin'"), [Regex]::Escape("@owner_login_name=N'$saLogin'") + } + + Write-Message -Message $sql -Level Debug + $destServer.Query($sql) + + $destServer.JobServer.Jobs.Refresh() + } + catch { + $copyJobStatus.Status = "Failed" + $copyJobStatus.Notes = (($_.Exception.InnerException.InnerException.Innerexception.InnerException).ToString().Split("`n"))[0] + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying job" -Target $jobName -ErrorRecord $_ -Continue + } + } + + if ($DisableOnDestination) { + if ($Pscmdlet.ShouldProcess($destination, "Disabling $jobName")) { + Write-Message -Message "Disabling $jobName on $destination" -Level Verbose + $destServer.JobServer.Jobs[$serverJob.name].IsEnabled = $False + $destServer.JobServer.Jobs[$serverJob.name].Alter() + } + } + + if ($DisableOnSource) { + if ($Pscmdlet.ShouldProcess($source, "Disabling $jobName")) { + Write-Message -Message "Disabling $jobName on $source" -Level Verbose + $serverJob.IsEnabled = $false + $serverJob.Alter() + } + } + $copyJobStatus.Status = "Successful" + $copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlJob + } +} +function Copy-DbaAgentOperator { + <# + .SYNOPSIS + Copy-DbaAgentOperator migrates operators from one SQL Server to another. + + .DESCRIPTION + By default, all operators are copied. The -Operators parameter is auto-populated for command-line completion and can be used to copy only specific operators. + + If the associated credentials for the operator do not exist on the destination, it will be skipped. If the operator already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Operator + The operator(s) to process. This list is auto-populated from the server. If unspecified, all operators will be processed. + + .PARAMETER ExcludeOperator + The operators(s) to exclude. This list is auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Operator will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent, Operator + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentOperator + + .EXAMPLE + Copy-DbaAgentOperator -Source sqlserver2014a -Destination sqlcluster + + Copies all operators from sqlserver2014a to sqlcluster using Windows credentials. If operators with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentOperator -Source sqlserver2014a -Destination sqlcluster -Operator PSOperator -SourceSqlCredential $cred -Force + + Copies only the PSOperator operator from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If an operator with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaAgentOperator -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Operator, + [object[]]$ExcludeOperator, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $serverOperator = $sourceServer.JobServer.Operators + $destOperator = $destServer.JobServer.Operators + + $failsafe = $destServer.JobServer.AlertSystem | Select-Object FailSafeOperator + } + process { + + foreach ($sOperator in $serverOperator) { + $operatorName = $sOperator.Name + + $copyOperatorStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $operatorName + Type = "Agent Operator" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Operator -and $Operator -notcontains $operatorName -or $ExcludeOperator -in $operatorName) { + continue + } + + if ($destOperator.Name -contains $sOperator.Name) { + if ($force -eq $false) { + $copyOperatorStatus.Status = "Skipped" + $copyOperatorStatus.Notes = "Already exists" + $copyOperatorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Operator $operatorName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($failsafe.FailSafeOperator -eq $operatorName) { + Write-Message -Level Verbose -Message "$operatorName is the failsafe operator. Skipping drop." + continue + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping operator $operatorName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping Operator $operatorName" + $destServer.JobServer.Operators[$operatorName].Drop() + } + catch { + $copyOperatorStatus.Status = "Failed" + $copyOperatorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping operator" -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating Operator $operatorName")) { + try { + Write-Message -Level Verbose -Message "Copying Operator $operatorName" + $sql = $sOperator.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copyOperatorStatus.Status = "Successful" + $copyOperatorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyOperatorStatus.Status = "Failed" + $copyOperatorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue creating operator." -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlOperator + } +} +function Copy-DbaAgentProxyAccount { + <# + .SYNOPSIS + Copy-DbaAgentProxyAccount migrates proxy accounts from one SQL Server to another. + + .DESCRIPTION + By default, all proxy accounts are copied. The -ProxyAccounts parameter is auto-populated for command-line completion and can be used to copy only specific proxy accounts. + + If the associated credential for the account does not exist on the destination, it will be skipped. If the proxy account already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Operator will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentProxyAccount + + .EXAMPLE + Copy-DbaAgentProxyAccount -Source sqlserver2014a -Destination sqlcluster + + Copies all proxy accounts from sqlserver2014a to sqlcluster using Windows credentials. If proxy accounts with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentProxyAccount -Source sqlserver2014a -Destination sqlcluster -ProxyAccount PSProxy -SourceSqlCredential $cred -Force + + Copies only the PSProxy proxy account from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a proxy account with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaAgentProxyAccount -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Server ProxyAccounts are only supported in SQL Server 2005 and above. Quitting." + } + + $serverProxyAccounts = $sourceServer.JobServer.ProxyAccounts + $destProxyAccounts = $destServer.JobServer.ProxyAccounts + } + process { + foreach ($proxyAccount in $serverProxyAccounts) { + $proxyName = $proxyAccount.Name + + $copyAgentProxyAccountStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $null + Type = "Agent Proxy" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($proxyAccounts.Length -gt 0 -and $proxyAccounts -notcontains $proxyName) { + continue + } + + # Proxy accounts rely on Credential accounts + $credentialName = $proxyAccount.CredentialName + $copyAgentProxyAccountStatus.Name = $credentialName + $copyAgentProxyAccountStatus.Type = "Credential" + + try { + $credentialtest = $destServer.Credentials[$CredentialName] + } + catch { + # don't care + } + + if ($null -eq $credentialtest) { + $copyAgentProxyAccountStatus.Status = "Skipped" + $copyAgentProxyAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Associated credential account, $CredentialName, does not exist on $destination. Skipping migration of $proxyName." + continue + } + + if ($destProxyAccounts.Name -contains $proxyName) { + $copyAgentProxyAccountStatus.Name = $proxyName + $copyAgentProxyAccountStatus.Type = "ProxyAccount" + + if ($force -eq $false) { + $copyAgentProxyAccountStatus.Status = "Skipped" + $copyAgentProxyAccountStatus + Write-Message -Level Verbose -Message "Server proxy account $proxyName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping server proxy account $proxyName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping server proxy account $proxyName" + $destServer.JobServer.ProxyAccounts[$proxyName].Drop() + } + catch { + $copyAgentProxyAccountStatus.Status = "Failed" + $copyAgentProxyAccountStatus.Notes = "Could not drop" + $copyAgentProxyAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping proxy account" -Target $proxyName -InnerErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating server proxy account $proxyName")) { + $copyAgentProxyAccountStatus.Name = $proxyName + $copyAgentProxyAccountStatus.Type = "ProxyAccount" + + try { + Write-Message -Level Verbose -Message "Copying server proxy account $proxyName" + $sql = $proxyAccount.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + # Will fixing this misspelled status cause problems downstream? + $copyAgentProxyAccountStatus.Status = "Successful" + $copyAgentProxyAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $exceptionstring = $_.Exception.InnerException.ToString() + if ($exceptionstring -match 'subsystem') { + $copyAgentProxyAccountStatus.Status = "Skipping" + $copyAgentProxyAccountStatus.Notes = "Failure" + $copyAgentProxyAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "One or more subsystems do not exist on the destination server. Skipping that part." + } + else { + $copyAgentProxyAccountStatus.Status = "Failed" + $copyAgentProxyAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating proxy account" -Target $proxyName -InnerErrorRecord $_ + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlProxyAccount + } +} +function Copy-DbaAgentSharedSchedule { + <# + .SYNOPSIS + Copy-DbaAgentSharedSchedule migrates shared job schedules from one SQL Server to another. + + .DESCRIPTION + All shared job schedules are copied. + + If the associated credential for the account does not exist on the destination, it will be skipped. If the shared job schedule already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Operator will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Agent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaAgentSharedSchedule + + .EXAMPLE + Copy-DbaAgentSharedSchedule -Source sqlserver2014a -Destination sqlcluster + + Copies all shared job schedules from sqlserver2014a to sqlcluster using Windows credentials. If shared job schedules with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaAgentSharedSchedule -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Server SharedSchedules are only supported in SQL Server 2005 and above. Quitting." + } + + $serverSchedules = $sourceServer.JobServer.SharedSchedules + $destSchedules = $destServer.JobServer.SharedSchedules + } + process { + foreach ($schedule in $serverSchedules) { + $scheduleName = $schedule.Name + $copySharedScheduleStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Agent Schedule" + Name = $scheduleName + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($schedules.Length -gt 0 -and $schedules -notcontains $scheduleName) { + continue + } + + if ($destSchedules.Name -contains $scheduleName) { + if ($force -eq $false) { + $copySharedScheduleStatus.Status = "Skipped" + $copySharedScheduleStatus.Notes = "Already exists" + $copySharedScheduleStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Shared job schedule $scheduleName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($destServer.JobServer.Jobs.JobSchedules.Name -contains $scheduleName) { + $copySharedScheduleStatus.Status = "Skipped" + $copySharedScheduleStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Schedule [$scheduleName] has associated jobs. Skipping." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping schedule $scheduleName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping schedule $scheduleName" + $destServer.JobServer.SharedSchedules[$scheduleName].Drop() + } + catch { + $copySharedScheduleStatus.Status = "Failed" + $copySharedScheduleStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping schedule" -Target $scheduleName -InnerErrorRecord $_ -Continue + } + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating schedule $scheduleName")) { + try { + Write-Message -Level Verbose -Message "Copying schedule $scheduleName" + $sql = $schedule.Script() | Out-String + + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copySharedScheduleStatus.Status = "Successful" + $copySharedScheduleStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copySharedScheduleStatus.Status = "Failed" + $copySharedScheduleStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue creating schedule" -Target $scheduleName -InnerErrorRecord $_ -Continue + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlSharedSchedule + } +} +function Copy-DbaBackupDevice { + <# + .SYNOPSIS + Copies backup devices one by one. Copies both SQL code and the backup file itself. + + .DESCRIPTION + Backups are migrated using Admin shares. If the destination directory does not exist, SQL Server's default backup directory will be used. + + If a backup device with same name exists on destination, it will not be dropped and recreated unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER BackupDevice + BackupDevice to be copied. Auto-populated list of devices. If not provided all BackupDevice(s) will be copied. + + .PARAMETER Force + If this switch is enabled, backup device(s) will be dropped and recreated if they already exists on destination. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Backup + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaBackupDevice + + .EXAMPLE + Copy-DbaBackupDevice -Source sqlserver2014a -Destination sqlcluster + + Copies all server backup devices from sqlserver2014a to sqlcluster using Windows credentials. If backup devices with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaBackupDevice -Source sqlserver2014a -Destination sqlcluster -BackupDevice backup01 -SourceSqlCredential $cred -Force + + Copies only the backup device named backup01 from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a backup device with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaBackupDevice -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$BackupDevice, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + $serverBackupDevices = $sourceServer.BackupDevices + $destBackupDevices = $destServer.BackupDevices + + $sourceNetBios = Resolve-NetBiosName $sourceServer + $destNetBios = Resolve-NetBiosName $destServer + } + process { + foreach ($currentBackupDevice in $serverBackupDevices) { + $deviceName = $currentBackupDevice.Name + + $copyBackupDeviceStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $deviceName + Type = "Backup Device" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($BackupDevice -and $BackupDevice -notcontains $deviceName) { + continue + } + + if ($destBackupDevices.Name -contains $deviceName) { + if ($force -eq $false) { + $copyBackupDeviceStatus.Status = "Skipped" + $copyBackupDeviceStatus.Notes = "Already exists" + $copyBackupDeviceStatus + + Write-Message -Level Verbose -Message "backup device $deviceName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping backup device $deviceName")) { + try { + Write-Message -Level Verbose -Message "Dropping backup device $deviceName" + $destServer.BackupDevices[$deviceName].Drop() + } + catch { + $copyBackupDeviceStatus.Status = "Failed" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping backup device" -Target $deviceName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Generating SQL code for $deviceName")) { + Write-Message -Level Verbose -Message "Scripting out SQL for $deviceName" + try { + $sql = $currentBackupDevice.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + } + catch { + $copyBackupDeviceStatus.Status = "Failed" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue scripting out backup device" -Target $deviceName -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess("console", "Stating that the actual file copy is about to occur")) { + Write-Message -Level Verbose -Message "Preparing to copy actual backup file" + } + + $path = Split-Path $sourceServer.BackupDevices[$deviceName].PhysicalLocation + $destPath = Join-AdminUnc $destNetBios $path + $sourcepath = Join-AdminUnc $sourceNetBios $sourceServer.BackupDevices[$deviceName].PhysicalLocation + + Write-Message -Level Verbose -Message "Checking if directory $destPath exists" + + if ($(Test-DbaSqlPath -SqlInstance $Destination -Path $path) -eq $false) { + $backupDirectory = $destServer.BackupDirectory + $destPath = Join-AdminUnc $destNetBios $backupDirectory + + if ($Pscmdlet.ShouldProcess($destination, "Updating create code to use new path")) { + Write-Message -Level Verbose -Message "$path doesn't exist on $destination" + Write-Message -Level Verbose -Message "Using default backup directory $backupDirectory" + + try { + Write-Message -Level Verbose -Message "Updating $deviceName to use $backupDirectory" + $sql = $sql -replace $path, $backupDirectory + } + catch { + $copyBackupDeviceStatus.Status = "Failed" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue updating script of backup device with new path" -Target $deviceName -ErrorRecord $_ -Continue + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Copying $sourcepath to $destPath using BITSTransfer")) { + try { + Start-BitsTransfer -Source $sourcepath -Destination $destPath -ErrorAction Stop + Write-Message -Level Verbose -Message "Backup device $deviceName successfully copied" + } + catch { + $copyBackupDeviceStatus.Status = "Failed" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue copying backup device to destination" -Target $deviceName -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Adding backup device $deviceName")) { + Write-Message -Level Verbose -Message "Adding backup device $deviceName on $destination" + try { + $destServer.Query($sql) + $destServer.BackupDevices.Refresh() + + $copyBackupDeviceStatus.Status = "Successful" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyBackupDeviceStatus.Status = "Failed" + $copyBackupDeviceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue adding backup device" -Target $deviceName -ErrorRecord $_ -Continue + } + } + } #end foreach backupDevice + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlBackupDevice + } +} +function Copy-DbaCentralManagementServer { + <# + .SYNOPSIS + Migrates SQL Server Central Management groups and server instances from one SQL Server to another. + + .DESCRIPTION + Copy-DbaCentralManagementServer copies all groups, subgroups, and server instances from one SQL Server to another. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER CMSGroup + This is an auto-populated array that contains your Central Management Server top-level groups on Source. You can specify one, many or none. + + If CMSGroup is not specified, all groups in your Central Management Server will be copied. + + .PARAMETER SwitchServerName + If this switch is enabled, all instance names will be changed from Source to Destination. + + Central Management Server does not allow you to add a shared registered server with the same name as the Configuration Server. + + .PARAMETER Force + If this switch is enabled, group(s) will be dropped and recreated if they already exists on destination. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaCentralManagementServer + + .EXAMPLE + Copy-DbaCentralManagementServer -Source sqlserver2014a -Destination sqlcluster + + All groups, subgroups, and server instances are copied from sqlserver's Central Management Server to sqlcluster's Central Management Server. + + .EXAMPLE + Copy-DbaCentralManagementServer -Source sqlserver2014a -Destination sqlcluster -ServerGroup Group1,Group3 + + Top-level groups Group1 and Group3 along with their subgroups and server instances are copied from sqlserver to sqlcluster. + + .EXAMPLE + Copy-DbaCentralManagementServer -Source sqlserver2014a -Destination sqlcluster -ServerGroup Group1,Group3 -SwitchServerName -SourceSqlCredential $SourceSqlCredential -DestinationSqlCredential $DestinationSqlCredential + + Top-level groups Group1 and Group3 along with their subgroups and server instances are copied from sqlserver to sqlcluster. When adding sql instances to sqlcluster, if the server name of the migrating instance is "sqlcluster", it will be switched to "sqlserver". + + If SwitchServerName is not specified, "sqlcluster" will be skipped. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$CMSGroup, + [switch]$SwitchServerName, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function Invoke-ParseServerGroup { + [cmdletbinding()] + param ( + $sourceGroup, + $destinationGroup, + $SwitchServerName + ) + if ($destinationGroup.Name -eq "DatabaseEngineServerGroup" -and $sourceGroup.Name -ne "DatabaseEngineServerGroup") { + $currentServerGroup = $destinationGroup + $groupName = $sourceGroup.Name + $destinationGroup = $destinationGroup.ServerGroups[$groupName] + + $copyDestinationGroupStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $groupName + Type = "CMS Destination Group" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($null -ne $destinationGroup) { + if ($force -eq $false) { + $copyDestinationGroupStatus.Status = "Skipped" + $copyDestinationGroupStatus.Notes = "Already exists" + $copyDestinationGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Level Verbose -Message "Destination group $groupName exists at destination. Use -Force to drop and migrate." + continue + } + if ($Pscmdlet.ShouldProcess($destination, "Dropping group $groupName")) { + try { + Write-Message -Level Verbose -Message "Dropping group $groupName" + $destinationGroup.Drop() + } + catch { + $copyDestinationGroupStatus.Status = "Failed" + $copyDestinationGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping group" -Target $groupName -InnerErrorRecord $_ -Continue + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating group $groupName")) { + Write-Message -Level Verbose -Message "Creating group $($sourceGroup.Name)" + $destinationGroup = New-Object Microsoft.SqlServer.Management.RegisteredServers.ServerGroup($currentServerGroup, $sourceGroup.Name) + $destinationGroup.Create() + + $copyDestinationGroupStatus.Status = "Successful" + $copyDestinationGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + # Add Servers + foreach ($instance in $sourceGroup.RegisteredServers) { + $instanceName = $instance.Name + $serverName = $instance.ServerName + + $copyInstanceStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $instanceName + Type = "CMS Instance" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($serverName.ToLower() -eq $toCmStore.DomainInstanceName.ToLower()) { + if ($SwitchServerName) { + $serverName = $fromCmStore.DomainInstanceName + $instanceName = $fromCmStore.DomainInstanceName + Write-Message -Level Verbose -Message "SwitchServerName was used and new CMS equals current server name. $($toCmStore.DomainInstanceName.ToLower()) changed to $serverName." + } + else { + $copyInstanceStatus.Status = "Skipped" + $copyInstanceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "$serverName is Central Management Server. Add prohibited. Skipping." + continue + } + } + + if ($destinationGroup.RegisteredServers.Name -contains $instanceName) { + if ($force -eq $false) { + $copyInstanceStatus.Status = "Skipped" + $copyInstanceStatus.Notes = "Already exists" + $copyInstanceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Instance $instanceName exists in group $groupName at destination. Use -Force to drop and migrate." + continue + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping instance $instanceName from $groupName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping instance $instance from $groupName" + $destinationGroup.RegisteredServers[$instanceName].Drop() + } + catch { + $copyInstanceStatus.Status = "Failed" + $copyInstanceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping instance from group" -Target $instanceName -InnerErrorRecord $_ -Continue + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Copying $instanceName")) { + $newServer = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServer($destinationGroup, $instanceName) + $newServer.ServerName = $serverName + $newServer.Description = $instance.Description + + if ($serverName -ne $fromCmStore.DomainInstanceName) { + $newServer.SecureConnectionString = $instance.SecureConnectionString.ToString() + $newServer.ConnectionString = $instance.ConnectionString.ToString() + } + + try { + $newServer.Create() + + $copyInstanceStatus.Status = "Successful" + $copyInstanceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyInstanceStatus.Status = "Failed" + $copyInstanceStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + if ($_.Exception -match "same name") { + Stop-Function -Message "Could not add Switched Server instance name." -Target $instanceName -InnerErrorRecord $_ -Continue + } + else { + Stop-Function -Message "Failed to add $serverName" -Target $instanceName -InnerErrorRecord $_ -Continue + } + } + Write-Message -Level Verbose -Message "Added Server $serverName as $instanceName to $($destinationGroup.Name)" + } + } + + # Add Groups + foreach ($fromSubGroup in $sourceGroup.ServerGroups) { + $fromSubGroupName = $fromSubGroup.Name + $toSubGroup = $destinationGroup.ServerGroups[$fromSubGroupName] + + $copyGroupStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $fromSubGroupName + Type = "CMS Group" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($null -ne $toSubGroup) { + if ($force -eq $false) { + $copyGroupStatus.Status = "Skipped" + $copyGroupStatus.Notes = "Already exists" + $copyGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Subgroup $fromSubGroupName exists at destination. Use -Force to drop and migrate." + continue + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping subgroup $fromSubGroupName recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping subgroup $fromSubGroupName" + $toSubGroup.Drop() + } + catch { + $copyGroupStatus.Status = "Failed" + $copyGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping subgroup" -Target $toSubGroup -InnerErrorRecord $_ -Continue + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating group $($fromSubGroup.Name)")) { + Write-Message -Level Verbose -Message "Creating group $($fromSubGroup.Name)" + $toSubGroup = New-Object Microsoft.SqlServer.Management.RegisteredServers.ServerGroup($destinationGroup, $fromSubGroup.Name) + $toSubGroup.create() + + $copyGroupStatus.Status = "Successful" + $copyGroupStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + + Invoke-ParseServerGroup -sourceGroup $fromSubGroup -destinationgroup $toSubGroup -SwitchServerName $SwitchServerName + } + } + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 10 -or $destServer.VersionMajor -lt 10) { + throw "Central Management Server is only supported in SQL Server 2008 and newer. Quitting." + } + } + + process { + Write-Message -Level Verbose -Message "Connecting to Central Management Servers" + + try { + $fromCmStore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sourceServer.ConnectionContext.SqlConnectionObject) + $toCmStore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($destServer.ConnectionContext.SqlConnectionObject) + } + catch { + throw "Cannot access Central Management Servers" + } + + $stores = $fromCmStore.DatabaseEngineServerGroup + if ($CMSGroup) { + $stores = @(); + foreach ($groupName in $CMSGroup) { + $stores += $fromCmStore.DatabaseEngineServerGroup.ServerGroups[$groupName] + } + } + + foreach ($store in $stores) { + Invoke-ParseServerGroup -sourceGroup $store -destinationgroup $toCmStore.DatabaseEngineServerGroup -SwitchServerName $SwitchServerName + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlCentralManagementServer + } +} +function Copy-DbaCredential { + <# + .SYNOPSIS + Copy-DbaCredential migrates SQL Server Credentials from one SQL Server to another while maintaining Credential passwords. + + .DESCRIPTION + By using password decryption techniques provided by Antti Rantasaari (NetSPI, 2014), this script migrates SQL Server Credentials from one server to another while maintaining username and password. + + Credit: https://blog.netspi.com/decrypting-mssql-database-link-server-passwords/ + License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Credential + This command requires access to the Windows OS via PowerShell remoting. Use this credential to connect to Windows using alternative credentials. + + .PARAMETER Name + Only include specific names + Note: if spaces exist in the credential name, you will have to type "" or '' around it. + + .PARAMETER ExcludeName + Excluded credential names + + .PARAMETER Identity + Only include specific identities + Note: if spaces exist in the credential identity, you will have to type "" or '' around it. + + .PARAMETER ExcludeIdentity + Excluded identities + + .PARAMETER Force + If this switch is enabled, the Credential will be dropped and recreated if it already exists on Destination. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: WSMan, Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: + - PowerShell Version 3.0, SQL Server SMO, + - Administrator access on Windows + - sysadmin access on SQL Server. + - DAC access enabled for local (default) + Limitations: Hasn't been tested thoroughly. Works on Win8.1 and SQL Server 2012 & 2014 so far. + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaCredential + + .EXAMPLE + Copy-DbaCredential -Source sqlserver2014a -Destination sqlcluster + + Copies all SQL Server Credentials on sqlserver2014a to sqlcluster. If Credentials exist on destination, they will be skipped. + + .EXAMPLE + Copy-DbaCredential -Source sqlserver2014a -Destination sqlcluster -Name "PowerShell Proxy Account" -Force + + Copies over one SQL Server Credential (PowerShell Proxy Account) from sqlserver to sqlcluster. If the Credential already exists on the destination, it will be dropped and recreated. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [PSCredential] + $Credential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [string[]]$Name, + [string[]]$ExcludeName, + [Alias('CredentialIdentity')] + [string[]]$Identity, + [Alias('ExcludeCredentialIdentity')] + [string[]]$ExcludeIdentity, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $null = Test-ElevationRequirement -ComputerName $Source.ComputerName + + function Get-SqlCredential { + <# + .SYNOPSIS + Gets Credential Logins + + This function is heavily based on Antti Rantasaari's script at http://goo.gl/omEOrW + Antti Rantasaari 2014, NetSPI + License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause + + .OUTPUT + System.Data.DataTable + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential + ) + + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + $sourceName = $server.Name + + # Query Service Master Key from the database - remove padding from the key + # key_id 102 eq service master key, thumbprint 3 means encrypted with machinekey + $sql = "SELECT substring(crypt_property,9,len(crypt_property)-8) FROM sys.key_encryptions WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001)" + try { + $smkBytes = $server.ConnectionContext.ExecuteScalar($sql) + } + catch { + throw "Can't execute SQL on $sourceName" + } + + $sourceNetBios = Resolve-NetBiosName $server + $instance = $server.InstanceName + $serviceInstanceId = $server.ServiceInstanceId + + # Get entropy from the registry - hopefully finds the right SQL server instance + try { + [byte[]]$entropy = Invoke-Command2 -ComputerName $sourceNetBios -Credential $Credential -ArgumentList $serviceInstanceId -ScriptBlock { + $serviceInstanceId = $args[0] + $entropy = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$serviceInstanceId\Security\").Entropy + return $entropy + } -Raw + } + catch { + throw "Can't access registry keys on $sourceName. Quitting. $_" + } + + # Decrypt the service master key + try { + $serviceKey = Invoke-Command2 -ComputerName $sourceNetBios -Credential $Credential -ArgumentList $smkBytes, $Entropy -ScriptBlock { + Add-Type -AssemblyName System.Security + Add-Type -AssemblyName System.Core + $smkBytes = $args[0]; $Entropy = $args[1] + $serviceKey = [System.Security.Cryptography.ProtectedData]::Unprotect($smkBytes, $Entropy, 'LocalMachine') + return $serviceKey + } -Raw + } + catch { + throw "Can't unprotect registry data on $($source.Name)). Quitting." + } + + <# + Choose the encryption algorithm based on the SMK length: + 3DES for 2008, AES for 2012 + Choose IV length based on the algorithm + #> + if (($serviceKey.Length -ne 16) -and ($serviceKey.Length -ne 32)) { + throw "Unknown key size. Cannot continue. Quitting." + } + + if ($serviceKey.Length -eq 16) { + $decryptor = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider + $ivlen = 8 + } + elseif ($serviceKey.Length -eq 32) { + $decryptor = New-Object System.Security.Cryptography.AESCryptoServiceProvider + $ivlen = 16 + } + + <# + Query link server password information from the Db. Remove header from pwdhash, + extract IV (as iv) and ciphertext (as pass). + Ignore links with blank credentials (integrated auth ?) + #> + if ($server.IsClustered -eq $false) { + $connString = "Server=ADMIN:$sourceNetBios\$instance;Trusted_Connection=True" + } + else { + $dacEnabled = $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue + + + if ($dacEnabled -eq $false) { + if ($Pscmdlet.ShouldProcess($server.Name, "Enabling DAC on clustered instance")) { + Write-Message -Level Verbose -Message "DAC must be enabled for clusters, even when accessed from active node. Enabling." + $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue = $true + $server.Configuration.Alter() + } + } + + $connString = "Server=ADMIN:$sourceName;Trusted_Connection=True" + } + + $sql = "SELECT QUOTENAME(name) AS name,credential_identity,substring(imageval,5,$ivlen) iv, substring(imageval,$($ivlen + 5),len(imageval)-$($ivlen + 4)) pass from sys.Credentials cred inner join sys.sysobjvalues obj on cred.credential_id = obj.objid where valclass=28 and valnum=2" + + # Get entropy from the registry + try { + $creds = Invoke-Command2 -ComputerName $sourceNetBios -Credential $Credential -ArgumentList $connString, $sql -ScriptBlock { + $connString = $args[0]; $sql = $args[1] + $conn = New-Object System.Data.SqlClient.SqlConnection($connString) + try { + $conn.Open() + $cmd = New-Object System.Data.SqlClient.SqlCommand($sql, $conn); + $data = $cmd.ExecuteReader() + $dt = New-Object "System.Data.DataTable" + $dt.Load($data) + $conn.Close() + $conn.Dispose() + return $dt + } + catch { + Stop-Function -Message "Can't establish local DAC connection to $sourceName or other error. Quitting." -ErrorRecord $_ + return + } + } -Raw + } + catch { + Stop-Function -Message "Can't establish local DAC connection to $sourceName or other error. Quitting." -ErrorRecord $_ + return + } + + if ($server.IsClustered -and $dacEnabled -eq $false) { + if ($Pscmdlet.ShouldProcess($server.Name, "Disabling DAC on clustered instance")) { + Write-Message -Level Verbose -Message "Setting DAC config back to 0" + $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue = $false + $server.Configuration.Alter() + } + } + + $decryptedLogins = New-Object "System.Data.DataTable" + [void]$decryptedLogins.Columns.Add("Credential") + [void]$decryptedLogins.Columns.Add("Identity") + [void]$decryptedLogins.Columns.Add("Password") + + # Go through each row in results + foreach ($cred in $creds) { + # decrypt the password using the service master key and the extracted IV + $decryptor.Padding = "None" + $decrypt = $decryptor.CreateEncryptor($serviceKey, $cred.iv) + $stream = New-Object System.IO.MemoryStream ( , $cred.pass) + $crypto = New-Object System.Security.Cryptography.CryptoStream $stream, $decrypt, "Write" + + $crypto.Write($cred.Pass, 0, $cred.Pass.Length) + [byte[]]$decrypted = $stream.ToArray() + + # convert decrypted password to unicode + $encode = New-Object System.Text.UnicodeEncoding + + <# + Print results - removing the weird padding (8 bytes in the front, some bytes at the end)... + Might cause problems but so far seems to work.. may be dependant on SQL server version... + If problems arise remove the next three lines.. + #> + $i = 8 + foreach ($b in $decrypted) { + if ($decrypted[$i] -ne 0 -and $decrypted[$i + 1] -ne 0 -or $i -eq $decrypted.Length) { + $i -= 1 + break + } + $i += 1 + } + $decrypted = $decrypted[8 .. $i] + + [void]$decryptedLogins.Rows.Add($($cred.Name), $($cred.Credential_Identity), $($encode.GetString($decrypted))) + } + return $decryptedLogins + } + + function Copy-Credential { + <# + .SYNOPSIS + Copies Credentials from one server to another using a combination of SMO's .Script() and manual password updates. + + .OUTPUT + System.Data.DataTable + #> + param ( + [string[]]$credentials, + [bool]$force + ) + + Write-Message -Level Verbose -Message "Collecting Credential logins and passwords on $($sourceServer.Name)" + $sourceCredentials = Get-SqlCredential $sourceServer + $credentialList = Get-DbaCredential -SqlInstance $sourceServer -Name $Name -ExcludeName $ExcludeName -Identity $Identity -ExcludeIdentity $ExcludeIdentity + + Write-Message -Level Verbose -Message "Starting migration" + foreach ($credential in $credentialList) { + $destServer.Credentials.Refresh() + $credentialName = $credential.Name + + $copyCredentialStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Credential" + Name = $credentialName + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destServer.Credentials[$credentialName]) { + if (!$force) { + $copyCredentialStatus.Status = "Skipping" + $copyCredentialStatus.Notes = "Already exists" + $copyCredentialStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "$credentialName exists $($destServer.Name). Skipping." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination.Name, "Dropping $identity")) { + $destServer.Credentials[$credentialName].Drop() + $destServer.Credentials.Refresh() + } + } + } + + Write-Message -Level Verbose -Message "Attempting to migrate $credentialName" + try { + $currentCred = $sourceCredentials | Where-Object { $_.Credential -eq "[$credentialName]" } + $identity = $currentCred.Identity + $password = $currentCred.Password + if ($Pscmdlet.ShouldProcess($destination.Name, "Copying $identity")) { + $sql = "CREATE CREDENTIAL [$credentialName] WITH IDENTITY = N'$identity', SECRET = N'$password'" + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + $destServer.Credentials.Refresh() + Write-Message -Level Verbose -Message "$credentialName successfully copied" + } + + $copyCredentialStatus.Status = "Successful" + $copyCredentialStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyCredentialStatus.Status = "Failed" + $copyCredentialStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Error creating credential" -Target $credentialName -ErrorRecord $_ + } + } + } + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($null -ne $SourceSqlCredential.Username) { + Write-Message -Level Verbose -Message "You are using SQL credentials and this script requires Windows admin access to the $Source server. Trying anyway." + } + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Credentials are only supported in SQL Server 2005 and above. Quitting." + } + + Invoke-SmoCheck -SqlInstance $sourceServer + Invoke-SmoCheck -SqlInstance $destServer + } + process { + if (Test-FunctionInterrupt) { return } + Write-Message -Level Verbose -Message "Getting NetBios name for $source" + $sourceNetBios = Resolve-NetBiosName $sourceServer + + # This output is wrong. Will fix later. + Write-Message -Level Verbose -Message "Checking if Remote Registry is enabled on $source" + try { + Invoke-Command2 -ComputerName $sourceNetBios -Credential $credential -ScriptBlock { Get-ItemProperty -Path "HKLM:\SOFTWARE\" } + } + catch { + Stop-Function -Message "Can't connect to registry on $source." -Target $sourceNetBios -ErrorRecord $_ + return + } + + # Magic happens here + Copy-Credential $credentials -force:$force + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlCredential + } +} +function Copy-DbaCustomError { + <# + .SYNOPSIS + Copy-DbaCustomError migrates custom errors (user defined messages), by the custom error ID, from one SQL Server to another. + + .DESCRIPTION + By default, all custom errors are copied. The -CustomError parameter is auto-populated for command-line completion and can be used to copy only specific custom errors. + + If the custom error already exists on the destination, it will be skipped unless -Force is used. The us_english version must be created first. If you drop the us_english version, all the other languages will be dropped for that specific ID as well. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER CustomError + The custom error(s) to process. This list is auto-populated from the server. If unspecified, all custom errors will be processed. + + .PARAMETER ExcludeCustomError + The custom error(s) to exclude. This list is auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, the custom error will be dropped and recreated if it already exists on Destination. + + .NOTES + Tags: Migration, CustomError + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaCustomError + + .EXAMPLE + Copy-DbaCustomError -Source sqlserver2014a -Destination sqlcluster + + Copies all server custom errors from sqlserver2014a to sqlcluster using Windows credentials. If custom errors with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaCustomError -Source sqlserver2014a -SourceSqlCredential $scred -Destination sqlcluster -DestinationSqlCredential $dcred -CustomError 60000 -Force + + Copies only the custom error with ID number 60000 from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a custom error with the same name exists on sqlcluster, it will be updated because -Force was used. + + .EXAMPLE + Copy-DbaCustomError -Source sqlserver2014a -Destination sqlcluster -ExcludeCustomError 60000 -Force + + Copies all the custom errors found on sqlserver2014a except the custom error with ID number 60000 to sqlcluster. If a custom error with the same name exists on sqlcluster, it will be updated because -Force was used. + + .EXAMPLE + Copy-DbaCustomError -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$CustomError, + [object[]]$ExcludeCustomError, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Custom Errors are only supported in SQL Server 2005 and above. Quitting." + } + } + process { + + # US has to go first + $orderedCustomErrors = @($sourceServer.UserDefinedMessages | Where-Object Language -eq "us_english") + $orderedCustomErrors += $sourceServer.UserDefinedMessages | Where-Object Language -ne "us_english" + $destCustomErrors = $destServer.UserDefinedMessages + + foreach ($currentCustomError in $orderedCustomErrors) { + $customErrorId = $currentCustomError.ID + $language = $currentCustomError.Language.ToString() + + $copyCustomErrorStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Custom error" + Name = $currentCustomError + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ( $CustomError -and ($customErrorId -notin $CustomError -or $customErrorId -in $ExcludeCustomError) ) { + continue + } + + if ($destCustomErrors.ID -contains $customErrorId) { + if ($force -eq $false) { + $copyCustomErrorStatus.Status = "Skipped" + $copyCustomErrorStatus.Notes = "Already exists" + $copyCustomErrorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Custom error $customErrorId $language exists at destination. Use -Force to drop and migrate." + continue + } + else { + If ($Pscmdlet.ShouldProcess($destination, "Dropping custom error $customErrorId $language and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping custom error $customErrorId (drops all languages for custom error $customErrorId)" + $destServer.UserDefinedMessages[$customErrorId, $language].Drop() + } + catch { + $copyCustomErrorStatus.Status = "Failed" + $copyCustomErrorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping custom error" -Target $customErrorId -InnerErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating custom error $customErrorId $language")) { + try { + Write-Message -Level Verbose -Message "Copying custom error $customErrorId $language" + $sql = $currentCustomError.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copyCustomErrorStatus.Status = "Successful" + $copyCustomErrorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyCustomErrorStatus.Status = "Failed" + $copyCustomErrorStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating custom error" -Target $customErrorId -InnerErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlCustomError + } +} +function Copy-DbaDatabase { + <# + .SYNOPSIS + Migrates SQL Server databases from one SQL Server to another. + + .DESCRIPTION + This script provides the ability to migrate databases using detach/copy/attach or backup/restore. This script works with named instances, clusters and SQL Server Express Edition. + + By default, databases will be migrated to the destination SQL Server's default data and log directories. You can override this by specifying -ReuseSourceFolderStructure. Filestreams and filegroups are also migrated. Safety is emphasized. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Migrates only specified databases. This list is auto-populated from the server for tab completion. Multiple databases may be specified as a collection. + + .PARAMETER ExcludeDatabase + Excludes specified databases when performing -AllDatabases migrations. This list is auto-populated from the Source for tab completion. + + .PARAMETER AllDatabases + If this switch is enabled, all user databases will be migrated. System and support databases will not be migrated. Requires -BackupRestore or -DetachAttach. + + .PARAMETER BackupRestore + If this switch is enabled, the copy-only backup and restore method will be used to migrate the database(s). This method requires that you specify -NetworkShare in a valid UNC format (\\server\share). + + Backups will be immediately deleted after use unless -NoBackupCleanup is specified. + + .PARAMETER NetworkShare + Specifies the network location for the backup files. The SQL Server service accounts must have read/write permission on this path. + + .PARAMETER WithReplace + If this switch is enabled, the restore is executed with WITH REPLACE. + + .PARAMETER NoRecovery + If this switch is enabled, the restore is executed with WITH NORECOVERY. Ideal for staging. + + .PARAMETER NoBackupCleanup + If this switch is enabled, backups generated by this cmdlet will not be deleted after they are restored. The default behavior is to delete these backups. + + .PARAMETER NumberFiles + Number of files to split the backup. Default is 3. + + .PARAMETER DetachAttach + If this switch is enabled, the detach/copy/attach method is used to perform database migrations. No files are deleted on Source. If Destination attachment fails, the Source database will be reattached. File copies are performed over administrative shares (\\server\x$\mssql) using BITS. If a database is being mirrored, the mirror will be broken prior to migration. + + .PARAMETER Reattach + If this switch is enabled, all databases are reattached to Source after DetachAttach migration. + + .PARAMETER SetSourceReadOnly + If this switch is enabled, all migrated databases are set to ReadOnly on Source prior to detach/attach & backup/restore. + + If -Reattach is used, databases are set to read-only after reattaching. + + .PARAMETER ReuseSourceFolderStructure + If this switch is enabled, databases will be migrated to a data and log directory structure on Destination mirroring that used on Source. By default, the default data and log directories for Destination will be used when the databases are migrated. + + The structure on Source will be kept exactly, so consider this if you're migrating between different versions and use part of Microsoft's default Sql structure (MSSql12.INSTANCE, etc) + + To reuse Destination folder structure, use the -WithReplace switch. + + .PARAMETER IncludeSupportDbs + If this switch is enabled, ReportServer, ReportServerTempDb, SSISDB, and distribution databases will be copied if they exist on Source. A log file named $SOURCE-$DESTINATION-$date-Sqls.csv will be written to the current directory. + + Use of this switch requires -BackupRestore or -DetachAttach as well. + + .PARAMETER DatabaseCollection + A collection of dbobjects from the pipeline. + + .PARAMETER NoCopyOnly + If this switch is enabled, backups will be taken without COPY_ONLY. This will break the LSN backup chain, which will interfere with the restore chain of the database. + + By default this switch is disabled, so backups will be taken with COPY_ONLY. This will preserve the LSN backup chain. + + For more details please refer to this MSDN article - https://msdn.microsoft.com/en-us/library/ms191495.aspx + + .PARAMETER SetSourceOffline + If this switch is enabled, the Source database will be set to Offline after being copied. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing databases on Destination with matching names from Source will be dropped. If using -DetachReattach, mirrors will be broken and the database(s) dropped from Availability Groups. + + .NOTES + Tags: Migration, Backup, Restore + Author: Chrissy LeMaire (@cl), netnerds.net + + Requires: sysadmin access on SQL Servers + Limitations: Doesn't cover what it doesn't cover (replication, certificates, etc) + + SQL Server 2000 databases cannot be directly migrated to SQL Server 2012 and above. + Logins within SQL Server 2012 and above logins cannot be migrated to SQL Server 2008 R2 and below. + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaDatabase + + .EXAMPLE + Copy-DbaDatabase -Source sqlserver2014a -Destination sqlserver2014b -Database TestDB -BackupRestore -NetworkShare \\fileshare\sql\migration + + Migrates a single user database TestDB using Backup and restore from instance sqlserver2014a to sqlserver2014b. Backup files are stored in \\fileshare\sql\migration. + + .EXAMPLE + Copy-DbaDatabase -Source sqlserver2014a -Destination sqlcluster -DetachAttach -Reattach + + Databases will be migrated from sqlserver2014a to sqlcluster using the detach/copy files/attach method.The following will be performed: kick all users out of the database, detach all data/log files, move files across the network over an admin share (\\SqlSERVER\M$\MSSql...), attach file on destination server, reattach at source. If the database files (*.mdf, *.ndf, *.ldf) on *destination* exist and aren't in use, they will be overwritten. + + .EXAMPLE + Copy-DbaDatabase -Source sqlserver2014a -Destination sqlcluster -ExcludeDatabase Northwind, pubs -IncludeSupportDbs -Force -BackupRestore -NetworkShare \\fileshare\sql\migration + + Migrates all user databases except for Northwind and pubs by using backup/restore (copy-only). Backup files are stored in \\fileshare\sql\migration. If the database exists on the destination, it will be dropped prior to attach. + + It also includes the support databases (ReportServer, ReportServerTempDb, distribution). + #> + [CmdletBinding(DefaultParameterSetName = "DbBackup", SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $false)] + [DbaInstanceParameter] + $Source, + + [PSCredential] + + $SourceSqlCredential, + + [parameter(Mandatory = $true)] + [DbaInstanceParameter] + $Destination, + + [PSCredential] + + $DestinationSqlCredential, + + [Alias("Databases")] + [object[]] + $Database, + + [object[]] + $ExcludeDatabase, + + [Alias("All")] + [parameter(ParameterSetName = "DbBackup")] + [parameter(ParameterSetName = "DbAttachDetach")] + [switch] + $AllDatabases, + + [parameter(Mandatory = $true, ParameterSetName = "DbBackup")] + [switch] + $BackupRestore, + + [parameter(Mandatory = $true, ParameterSetName = "DbBackup", + HelpMessage = "Specify a valid network share in the format \\server\share that can be accessed by your account and the SQL Server service accounts for both Source and Destination.")] + [string] + $NetworkShare, + + [parameter(ParameterSetName = "DbBackup")] + [switch] + $WithReplace, + + [parameter(ParameterSetName = "DbBackup")] + [switch] + $NoRecovery, + + [parameter(ParameterSetName = "DbBackup")] + [switch] + $NoBackupCleanup, + + [parameter(ParameterSetName = "DbBackup")] + [ValidateRange(1, 64)] + [int] + $NumberFiles = 3, + + [parameter(Mandatory = $true, ParameterSetName = "DbAttachDetach")] + [switch] + $DetachAttach, + + [parameter(ParameterSetName = "DbAttachDetach")] + [switch] + $Reattach, + + [parameter(ParameterSetName = "DbBackup")] + [parameter(ParameterSetName = "DbAttachDetach")] + [switch] + $SetSourceReadOnly, + + [Alias("ReuseFolderStructure")] + [parameter(ParameterSetName = "DbBackup")] + [parameter(ParameterSetName = "DbAttachDetach")] + [switch] + $ReuseSourceFolderStructure, + + [parameter(ParameterSetName = "DbBackup")] + [parameter(ParameterSetName = "DbAttachDetach")] + [switch] + $IncludeSupportDbs, + + [parameter(ValueFromPipeline = $True)] + [object] + $DatabaseCollection, + + [switch] + $NoCopyOnly, + + [switch] + $SetSourceOffline, + + [switch] + $Force, + + [switch] + [Alias('Silent')]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlDatabase + + $CopyOnly = -not $NoCopyOnly + function Join-AdminUnc { + <# + .SYNOPSIS + Internal function. Parses a path to make it an admin UNC. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $servername, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $filepath + + ) + + if ($script:sameserver) { return $filepath } + if (!$filepath) { return } + if ($filepath.StartsWith("\\")) { return $filepath } + + $servername = $servername.Split("\")[0] + + if ($filepath.length -gt 0 -and $filepath -ne [System.DbNull]::Value) { + $newpath = Join-Path "\\$servername\" $filepath.replace(':', '$') + return $newpath + } + else { return } + } + + # Global Database Function + function Get-SqlFileStructure { + $dbcollection = @{ }; + $databaseProgressbar = 0 + + foreach ($db in $databaseList) { + Write-Progress -Id 1 -Activity "Processing database file structure" -PercentComplete ($databaseProgressbar / $dbCount * 100) -Status "Processing $databaseProgressbar of $dbCount." + $dbName = $db.Name + Write-Message -Level Verbose -Message $dbName + + $databaseProgressbar++ + $dbStatus = $db.status.toString() + if ($dbStatus.StartsWith("Normal") -eq $false) { continue } + $destinationfiles = @{ }; $sourcefiles = @{ } + + $where = "Filetype <> 'LOG' and Filetype <> 'FULLTEXT'" + + $datarows = $dbFileTable.Tables.Select("dbname = '$dbName' and $where") + + # Data Files + foreach ($file in $datarows) { + # Destination File Structure + $d = @{ } + if ($ReuseSourceFolderStructure) { + $d.physical = $file.filename + } + elseif ($WithReplace) { + $name = $file.Name + $destfile = $remoteDbFileTable.Tables[0].Select("dbname = '$dbName' and name = '$name'") + $d.physical = $destfile.filename + + if ($null -eq $d.physical) { + $directory = Get-SqlDefaultPaths $destServer data + $fileName = Split-Path $file.filename -Leaf + $d.physical = "$directory\$fileName" + } + } + else { + $directory = Get-SqlDefaultPaths $destServer data + $fileName = Split-Path $file.filename -Leaf + $d.physical = "$directory\$fileName" + } + $d.logical = $file.Name + + $d.remotefilename = Join-AdminUNC $destNetBios $d.physical + $destinationfiles.add($file.Name, $d) + + # Source File Structure + $s = @{ } + $s.logical = $file.Name + $s.physical = $file.filename + $s.remotefilename = Join-AdminUNC $sourceNetBios $s.physical + $sourcefiles.add($file.Name, $s) + } + + # Add support for Full Text Catalogs in SQL Server 2005 and below + if ($sourceServer.VersionMajor -lt 10) { + try { + $fttable = $null = $sourceServer.Databases[$dbName].ExecuteWithResults('sp_help_fulltext_catalogs') + $allrows = $fttable.Tables[0].rows + } + catch { + # Nothing, it's just not enabled + } + + foreach ($ftc in $allrows) { + # Destination File Structure + $d = @{ } + $pre = "sysft_" + $name = $ftc.Name + $physical = $ftc.Path # RootPath + $logical = "$pre$name" + if ($ReuseSourceFolderStructure) { + $d.physical = $physical + } + else { + $directory = Get-SqlDefaultPaths $destServer data + if ($destServer.VersionMajor -lt 10) { $directory = "$directory\FTDATA" } + $fileName = Split-Path($physical) -leaf + $d.physical = "$directory\$fileName" + } + $d.logical = $logical + $d.remotefilename = Join-AdminUNC $destNetBios $d.physical + $destinationfiles.add($logical, $d) + + # Source File Structure + $s = @{ } + $pre = "sysft_" + $name = $ftc.Name + $physical = $ftc.Path # RootPath + $logical = "$pre$name" + + $s.logical = $logical + $s.physical = $physical + $s.remotefilename = Join-AdminUNC $sourceNetBios $s.physical + $sourcefiles.add($logical, $s) + } + } + + $where = "Filetype = 'LOG'" + $datarows = $dbFileTable.Tables[0].Select("dbname = '$dbName' and $where") + + # Log Files + foreach ($file in $datarows) { + $d = @{ } + if ($ReuseSourceFolderStructure) { + $d.physical = $file.filename + } + elseif ($WithReplace) { + $name = $file.Name + $destfile = $remoteDbFileTable.Tables[0].Select("dbname = '$dbName' and name = '$name'") + $d.physical = $destfile.filename + + if ($null -eq $d.physical) { + $directory = Get-SqlDefaultPaths $destServer data + $fileName = Split-Path $file.filename -Leaf + $d.physical = "$directory\$fileName" + } + } + else { + $directory = Get-SqlDefaultPaths $destServer log + $fileName = Split-Path $file.filename -Leaf + $d.physical = "$directory\$fileName" + } + $d.logical = $file.Name + $d.remotefilename = Join-AdminUNC $destNetBios $d.physical + $destinationfiles.add($file.Name, $d) + + $s = @{ } + $s.logical = $file.Name + $s.physical = $file.filename + $s.remotefilename = Join-AdminUNC $sourceNetBios $s.physical + $sourcefiles.add($file.Name, $s) + } + + $location = @{ } + $location.add("Destination", $destinationfiles) + $location.add("Source", $sourcefiles) + $dbcollection.Add($($db.Name), $location) + } + + $fileStructure = [pscustomobject]@{ "databases" = $dbcollection } + Write-Progress -id 1 -Activity "Processing database file structure" -Status "Completed" -Completed + return $fileStructure + } + + # Detach Attach + function Dismount-SqlDatabase { + [CmdletBinding()] + param ( + [object] + $server, + + [string] + $dbName + ) + + $currentdb = $server.databases[$dbName] + if ($currentdb.IsMirroringEnabled) { + try { + Write-Message -Level Verbose -Message "Breaking mirror for $dbName" + $currentdb.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $currentdb.Alter() + $currentdb.Refresh() + Write-Message -Level Verbose -Message "Could not break mirror for $dbName. Skipping." + } + catch { + Stop-Function -Message "Issue breaking mirror." -Target $dbName -ErrorRecord $_ + return $false + } + } + + if ($currentdb.AvailabilityGroupName.Length -gt 0) { + $agName = $currentdb.AvailabilityGroupName + Write-Message -Level Verbose -Message "Attempting remove from Availability Group $agName." + try { + $server.AvailabilityGroups[$currentdb.AvailabilityGroupName].AvailabilityDatabases[$dbName].Drop() + Write-Message -Level Verbose -Message "Successfully removed $dbName from detach from $agName on $($server.Name)." + } + catch { + Stop-Function -Message "Could not remove $dbName from $agName on $($server.Name)." -Target $dbName -ErrorRecord $_ + return $false + } + } + + Write-Message -Level Verbose -Message "Attempting detach from $dbName from $source." + + ####### Using Sql to detach does not modify the $currentdb collection ####### + + $server.KillAllProcesses($dbName) + + try { + $sql = "ALTER DATABASE [$dbName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE" + Write-Message -Level Verbose -Message $sql + $null = $server.Query($sql) + Write-Message -Level Verbose -Message "Successfully set $dbName to single-user from $source." + } + catch { + Stop-Function -Message "Issue setting database to single-user." -Target $dbName -ErrorRecord $_ + } + + try { + $sql = "EXEC master.dbo.sp_detach_db N'$dbName'" + Write-Message -Level Verbose -Message $sql + $null = $server.Query($sql) + Write-Message -Level Verbose -Message "Successfully detached $dbName from $source." + return $true + } + catch { + Stop-Function -Message "Issue detaching database." -Target $dbName -ErrorRecord $_ + return $false + } + } + + function Mount-SqlDatabase { + [CmdletBinding()] + param ( + [object] + $server, + + [string] + $dbName, + + [object] + $fileStructure, + + [string] + $dbOwner + ) + + if ($null -eq $server.Logins.Item($dbOwner)) { + try { + $dbOwner = ($destServer.logins | Where-Object { $_.id -eq 1 }).Name + } + catch { + $dbOwner = "sa" + } + } + try { + $null = $server.AttachDatabase($dbName, $fileStructure, $dbOwner, [Microsoft.SqlServer.Management.Smo.AttachOptions]::None) + return $true + } + catch { + Stop-Function -Message "Issue mounting database." -ErrorRecord $_ + return $false + } + } + + function Start-SqlFileTransfer { + <# + + SYNOPSIS + Internal function. Uses BITS to transfer detached files (.mdf, .ndf, .ldf, and filegroups) to + another server over admin UNC paths. Locations of data files are kept in the + custom object generated by Get-SqlFileStructure + + #> + param ( + [object] + $fileStructure, + + [string] + $dbName + ) + + $copydb = $fileStructure.databases[$dbName] + $dbsource = $copydb.source + $dbdestination = $copydb.destination + + foreach ($file in $dbsource.keys) { + $remotefilename = $dbdestination[$file].remotefilename + $from = $dbsource[$file].remotefilename + try { + if (Test-Path $from -pathtype container) { + $null = New-Item -ItemType Directory -Path $remotefilename -Force + Start-BitsTransfer -Source "$from\*.*" -Destination $remotefilename + + $directories = (Get-ChildItem -recurse $from | Where-Object { $_.PsIsContainer }).FullName + foreach ($directory in $directories) { + $newdirectory = $directory.replace($from, $remotefilename) + $null = New-Item -ItemType Directory -Path $newdirectory -Force + Start-BitsTransfer -Source "$directory\*.*" -Destination $newdirectory + } + } + else { + Write-Message -Level Verbose -Message "Copying $from for $dbName." + Start-BitsTransfer -Source $from -Destination $remotefilename + } + } + catch { + try { + # Sometimes BITS trips out temporarily on cloned drives. + Start-BitsTransfer -Source $from -Destination $remotefilename + } + catch { + Write-Message -Level Verbose -Message "Start-BitsTransfer did not succeed. Now attempting with Copy-Item - no progress bar will be shown." + try { + Copy-Item -Path $from -Destination $remotefilename -ErrorAction Stop + } + catch { + Write-Message -Level Verbose -Message "Access denied. This can happen for a number of reasons including issues with cloned disks." + Stop-Function -Message "Alternatively, you may need to run PowerShell as Administrator, especially when running on localhost." -Target $from -ErrorRecord $_ + return + } + } + } + } + return $true + } + + function Start-SqlDetachAttach { + <# + + .SYNOPSIS + Internal function. Performs checks, then executes Dismount-SqlDatabase on a database, copies its files to the new server, then performs Mount-SqlDatabase. $sourceServer and $destServer are SMO server objects. + + $fileStructure is a custom object generated by Get-SqlFileStructure + + #> + [CmdletBinding()] + param ( + [object] + $sourceServer, + + [object] + $destServer, + + [object] + $fileStructure, + + [string] + $dbName + ) + + $destfilestructure = New-Object System.Collections.Specialized.StringCollection + $sourceFileStructure = New-Object System.Collections.Specialized.StringCollection + $dbOwner = $sourceServer.databases[$dbName].owner + + if ($null -eq $dbOwner) { + try { + $dbOwner = ($destServer.logins | Where-Object { $_.id -eq 1 }).Name + } + catch { + $dbOwner = "sa" + } + } + + foreach ($file in $fileStructure.databases[$dbName].destination.values) { $null = $destfilestructure.add($file.physical) } + foreach ($file in $fileStructure.databases[$dbName].source.values) { $null = $sourceFileStructure.add($file.physical) } + + $detachresult = Dismount-SqlDatabase $sourceServer $dbName + + if ($detachresult) { + $transfer = Start-SqlFileTransfer $fileStructure $dbName + if ($transfer -eq $false) { Write-Warning "Could not copy files."; return "Could not copy files." } + $attachresult = Mount-SqlDatabase $destServer $dbName $destfilestructure $dbOwner + + if ($attachresult -eq $true) { + # add to added dbs because ATTACH was successful + Write-Message -Level Verbose -Message "Successfully attached $dbName to $destination." + return $true + } + else { + # add to failed because ATTACH was unsuccessful + Write-Message -Level Verbose -Message "Could not attach $dbName." + return "Could not attach database." + } + } + else { + # add to failed because DETACH was unsuccessful + Write-Message -Level Verbose -Message "Could not detach $dbName." + return "Could not detach database." + } + } + + } + process { + if (Test-FunctionInterrupt) { return } + + if (($AllDatabases -or $IncludeSupportDbs -or $Database) -and !$DetachAttach -and !$BackupRestore) { + Stop-Function -Message "You must specify -DetachAttach or -BackupRestore when migrating databases." + return + } + + if ($DatabaseCollection.Length -gt 0) { + $Source = $DatabaseCollection[0].Parent.Name + $Database = $DatabaseCollection.Name + } + + if ($Database -contains "master" -or $Database -contains "msdb" -or $Database -contains "tempdb") { + Stop-Function -Message "Migrating system databases is not currently supported." + return + } + + if (!$AllDatabases -and !$IncludeSupportDbs -and !$Database) { + Stop-Function -Message "You must specify a -AllDatabases or -Database to continue." + return + } + + Write-Message -Level Verbose -Message "Attempting to connect to SQL Servers." + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + if ($sourceServer.ComputerNamePhysicalNetBIOS -eq $destServer.ComputerNamePhysicalNetBIOS) { + $script:sameserver = $true + } + else { + $script:sameserver = $false + } + + if ($script:sameserver -and $DetachAttach) { + if (-not (Test-ElevationRequirement -ComputerName $sourceServer)) { return } + } + + $destVersionLower = $destServer.VersionMajor -lt $sourceServer.VersionMajor + $destVersionMinorLow = ($destServer.VersionMajor -eq 10 -and $sourceServer.VersionMajor -eq 10) -and ($destServer.VersionMinor -lt $sourceServer.VersionMinor) + + if ($destVersionLower -or $destVersionMinorLow) { + Stop-Function -Message "Error: copy database cannot be made from newer $($sourceServer.VersionString) to older $($destServer.VersionString) SQL Server version." + return + } + + if ($DetachAttach) { + if ($sourceServer.NetName -eq $env:COMPUTERNAME -or $destServer.NetName -eq $env:COMPUTERNAME) { + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Message -Level Verbose -Message "When running DetachAttach locally on the console, it's possible you'll need to Run As Administrator. Trying anyway." + } + } + } + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($NetworkShare.Length -gt 0) { + if ($(Test-DbaSqlPath -SqlInstance $sourceServer -Path $NetworkShare) -eq $false) { + Write-Message -Level Verbose -Message "$Source may not be able to access $NetworkShare. Trying anyway." + } + + if ($(Test-DbaSqlPath -SqlInstance $destServer -Path $NetworkShare) -eq $false) { + Write-Message -Level Verbose -Message "$Destination may not be able to access $NetworkShare. Trying anyway." + } + + if ($NetworkShare.StartsWith('\\')) { + try { + $shareServer = ($NetworkShare -split "\\")[2] + $hostEntry = ([Net.Dns]::GetHostEntry($shareServer)).HostName -split "\." + + if ($shareServer -ne $hostEntry[0]) { + Write-Message -Level Verbose -Message "Using CNAME records for the network share may present an issue if an SPN has not been created. Trying anyway. If it doesn't work, use a different (A record) hostname." + } + } + catch { + Stop-Function -Message "Error validating unc path: $_" + return + } + } + } + + Write-Message -Level Verbose -Message "Resolving NetBIOS names." + $sourceNetBios = Resolve-NetBiosName $sourceServer + $destNetBios = Resolve-NetBiosName $destServer + + Write-Message -Level Verbose -Message "Performing SMO version check." + Invoke-SmoCheck -SqlInstance $sourceServer + Invoke-SmoCheck -SqlInstance $destServer + + Write-Message -Level Verbose -Message "Checking to ensure the source isn't the same as the destination." + if ($source -eq $destination) { + Stop-Function -Message "Source and Destination SQL Servers instances are the same. Quitting." + return + } + + if ($NetworkShare.Length -gt 0) { + Write-Message -Level Verbose -Message "Checking to ensure network path is valid." + if (!($NetworkShare.StartsWith("\\")) -and !$script:sameserver) { + Stop-Function -Message "Network share must be a valid UNC path (\\server\share)." + return + } + + if (!$script:sameserver) { + try { + if ((Test-Path $NetworkShare -ErrorAction Stop)) { + Write-Message -Level Verbose -Message "$NetworkShare share can be accessed." + } + } + catch { + Write-Message -Level Verbose -Message "$NetworkShare share cannot be accessed. Still trying anyway, in case the SQL Server service accounts have access." + } + } + } + + Write-Message -Level Verbose -Message "Checking to ensure server is not SQL Server 7 or below." + if ($sourceServer.VersionMajor -lt 8 -and $destServer.VersionMajor -lt 8) { + Stop-Function -Message "This script can only be run on SQL Server 2000 and above. Quitting." + return + } + + Write-Message -Level Verbose -Message "Checking to ensure detach/attach is not attempted on SQL Server 2000." + if ($destServer.VersionMajor -lt 9 -and $DetachAttach) { + Stop-Function -Message "Detach/Attach not supported when destination SQL Server is version 2000. Quitting." -Target $destServer + return + } + + Write-Message -Level Verbose -Message "Checking to ensure SQL Server 2000 migration isn't directly attempted to SQL Server 2012." + if ($sourceServer.VersionMajor -lt 9 -and $destServer.VersionMajor -gt 10) { + Stop-Function -Message "SQL Server 2000 databases cannot be migrated to SQL Server versions 2012 and above. Quitting." -Target $destServer + return + } + + Write-Message -Level Verbose -Message "Warning if migration from 2005 to 2012 and above and attach/detach is used." + if ($sourceServer.VersionMajor -eq 9 -and $destServer.VersionMajor -gt 9 -and !$BackupRestore -and !$Force -and $DetachAttach) { + Stop-Function -Message "Backup and restore is the safest method for migrating from SQL Server 2005 to other SQL Server versions. Please use the -BackupRestore switch or override this requirement by specifying -Force." + return + } + + if ($sourceServer.Collation -ne $destServer.Collation) { + Write-Message -Level Verbose -Message "Warning on different collation." + Write-Message -Level Verbose -Message "Collation on $Source, $($sourceServer.Collation) differs from the $Destination, $($destServer.Collation)." + } + + Write-Message -Level Verbose -Message "Ensuring user databases exist (counting databases)." + $dbTotal = $sourceServer.Databases.Count + + if ($dbTotal -le 4) { + Stop-Function -Message "No user databases to migrate. Quitting." + } + + Write-Message -Level Verbose -Message "Ensuring destination server version is equal to or greater than source." + if ($sourceServer.VersionMajor -ge $destServer.VersionMajor) { + if ($sourceServer.VersionMinor -gt $destServer.VersionMinor) { + Stop-Function -Message "Source SQL Server version build must be <= destination SQL Server for database migration." + } + } + + # SMO's filestreamlevel is sometimes null + $sql = "select coalesce(SERVERPROPERTY('FilestreamConfiguredLevel'),0) as fs" + $sourceFilestream = $sourceServer.ConnectionContext.ExecuteScalar($sql) + $destFilestream = $destServer.ConnectionContext.ExecuteScalar($sql) + if ($sourceFilestream -gt 0 -and $destFilestream -eq 0) { + $fsWarning = $true + } + + Write-Message -Level Verbose -Message "Writing warning about filestream being enabled." + if ($fsWarning) { + Write-Message -Level Verbose -Message "FILESTREAM enabled on $source but not $destination. Databases that use FILESTREAM will be skipped." + } + + if ($DetachAttach -eq $true) { + Write-Message -Level Verbose -Message "Checking access to remote directories." + $remoteSourcePath = Join-AdminUNC $sourceNetBios (Get-SqlDefaultPaths -SqlInstance $sourceServer -filetype data) + + if ((Test-Path $remoteSourcePath) -ne $true -and $DetachAttach) { + Write-Message -Level Verbose -Message "Can't access remote Sql directories on $source which is required to perform detach/copy/attach." + Write-Message -Level Verbose -Message "You can manually try accessing $remoteSourcePath to diagnose any issues." + Stop-Function -Message "Halting database migration." + return + } + + $remoteDestPath = Join-AdminUNC $destNetBios (Get-SqlDefaultPaths -SqlInstance $destServer -filetype data) + If ((Test-Path $remoteDestPath) -ne $true -and $DetachAttach) { + Write-Message -Level Verbose -Message "Can't access remote Sql directories on $destination which is required to perform detach/copy/attach." + Write-Message -Level Verbose -Message "You can manually try accessing $remoteDestPath to diagnose any issues." + Stop-Function -Message "Halting database migration." + return + } + } + + if (($Database -or $ExcludeDatabase -or $IncludeSupportDbs) -and (!$DetachAttach -and !$BackupRestore)) { + Stop-Function -Message "You did not select a migration method. Please use -BackupRestore or -DetachAttach." + return + } + + if ((!$Database -and !$AllDatabases -and !$IncludeSupportDbs) -and ($DetachAttach -or $BackupRestore)) { + Stop-Function -Message "You did not select any databases to migrate. Please use -AllDatabases or -Database or -IncludeSupportDbs." + return + } + + Write-Message -Level Verbose -Message "Building database list." + $databaseList = New-Object System.Collections.ArrayList + $SupportDBs = "ReportServer", "ReportServerTempDB", "distribution" + foreach ($currentdb in ($sourceServer.Databases | Where-Object IsAccessible)) { + $dbName = $currentdb.Name + $dbOwner = $currentdb.Owner + + if ($currentdb.Id -le 4) { continue } + if ($Database -and $Database -notcontains $dbName) { continue } + if ($IncludeSupportDBs -eq $false -and $SupportDBs -contains $dbName) { continue } + if ($IncludeSupportDBs -eq $true -and $SupportDBs -notcontains $dbName) { + if ($AllDatabases -eq $false -and $Database.length -eq 0) { continue } + } + $null = $databaseList.Add($currentdb) + } + + Write-Message -Level Verbose -Message "Performing count." + $dbCount = $databaseList.Count + + Write-Message -Level Verbose -Message "Building file structure inventory for $dbCount databases." + + if ($sourceServer.VersionMajor -eq 8) { + $sql = "select DB_NAME (dbid) as dbname, name, filename, CASE WHEN groupid = 0 THEN 'LOG' ELSE 'ROWS' END as filetype from sysaltfiles" + } + else { + $sql = "SELECT db.Name AS dbname, type_desc AS FileType, mf.Name, Physical_Name AS filename FROM sys.master_files mf INNER JOIN sys.databases db ON db.database_id = mf.database_id" + } + + $dbFileTable = $sourceServer.Databases['master'].ExecuteWithResults($sql) + + if ($destServer.VersionMajor -eq 8) { + $sql = "select DB_NAME (dbid) as dbname, name, filename, CASE WHEN groupid = 0 THEN 'LOG' ELSE 'ROWS' END as filetype from sysaltfiles" + } + else { + $sql = "SELECT db.Name AS dbname, type_desc AS FileType, mf.Name, Physical_Name AS filename FROM sys.master_files mf INNER JOIN sys.databases db ON db.database_id = mf.database_id" + } + + $remoteDbFileTable = $destServer.Databases['master'].ExecuteWithResults($sql) + + $fileStructure = Get-SqlFileStructure -sourceserver $sourceServer -destserver $destServer -databaselist $databaseList -ReuseSourceFolderStructure $ReuseSourceFolderStructure + + $elapsed = [System.Diagnostics.Stopwatch]::StartNew() + $started = Get-Date + $script:TimeNow = (Get-Date -UFormat "%m%d%Y%H%M%S") + + if ($AllDatabases -or $ExcludeDatabase.length -gt 0 -or $IncludeSupportDbs -or $Database.length -gt 0) { + foreach ($currentdb in $databaseList) { + $dbName = $currentdb.Name + $dbOwner = $currentdb.Owner + + $copyDatabaseStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $dbName + DestinationDatabase = $dbname + Type = "Database" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + Write-Message -Level Verbose -Message "`n######### Database: $dbName #########" + $dbStart = Get-Date + + if ($ExcludeDatabase -contains $dbName) { + Write-Message -Level Verbose -Message "$dbName excluded. Skipping." + continue + } + + Write-Message -Level Verbose -Message "Checking for accessibility." + if ($currentdb.IsAccessible -eq $false) { + Write-Message -Level Verbose -Message "Skipping $dbName. Database is inaccessible." + + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Database is not accessible" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($fsWarning) { + $fsRows = $dbFileTable.Tables[0].Select("dbname = '$dbName' and FileType = 'FileStream'") + + if ($fsRows.Count -gt 0) { + Write-Message -Level Verbose -Message "Skipping $dbName (contains FILESTREAM)." + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Contains FILESTREAM" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + } + + if ($ReuseSourceFolderStructure) { + $fgRows = $dbFileTable.Tables[0].Select("dbname = '$dbName' and FileType = 'ROWS'")[0] + $remotePath = Split-Path $fgRows.Filename + + if (!(Test-DbaSqlPath -SqlInstance $destServer -Path $remotePath)) { + # Stop-Function -Message "Cannot resolve $remotePath on $source. `n`nYou have specified ReuseSourceFolderStructure and exact folder structure does not exist. Halting script." + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "$remotePath does not exist on $destination and ReuseSourceFolderStructure was specified" #"Can't resolve $remotePath" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + return + } + } + + Write-Message -Level Verbose -Message "Checking Availability Group status." + if ($currentdb.AvailabilityGroupName.Length -gt 0 -and !$force -and $DetachAttach) { + $agName = $currentdb.AvailabilityGroupName + Write-Message -Level Verbose -Message "Database is part of an Availability Group ($agName). Use -Force to drop from $agName and migrate. Alternatively, you can use the safer backup/restore method." + continue + } + + $dbStatus = $currentdb.Status.ToString() + + if ($dbStatus.StartsWith("Normal") -eq $false) { + Write-Message -Level Verbose -Message "$dbName is not in a Normal state. Skipping." + + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Not in normal state" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($currentdb.ReplicationOptions -ne "None" -and $DetachAttach -eq $true) { + Write-Message -Level Verbose -Message "$dbName is part of replication. Skipping." + + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Part of replication" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($currentdb.IsMirroringEnabled -and !$force -and $DetachAttach) { + Write-Message -Level Verbose -Message "Database is being mirrored. Use -Force to break mirror and migrate. Alternatively, you can use the safer backup/restore method." + + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Database is mirrored" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if (($null -ne $destServer.Databases[$dbName]) -and !$force -and !$WithReplace) { + Write-Message -Level Verbose -Message "Database exists at destination. Use -Force to drop and migrate. Aborting routine for this database." + + $copyDatabaseStatus.Status = "Skipped" + $copyDatabaseStatus.Notes = "Already exists" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + elseif ($null -ne $destServer.Databases[$dbName] -and $force) { + if ($Pscmdlet.ShouldProcess($destination, "DROP DATABASE $dbName")) { + Write-Message -Level Verbose -Message "$dbName already exists. -Force was specified. Dropping $dbName on $destination." + $removeresult = Remove-DbaDatabase -SqlInstance $destserver -Database $dbname -Confirm:$false + $dropResult = $removeresult.Status -eq 'Dropped' + + if ($dropResult -eq $false) { + Write-Message -Level Verbose -Message "Database could not be dropped. Aborting routine for this database." + + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Could not drop database" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + } + } + + if ($force) { + $WithReplace = $true + } + + if ($Pscmdlet.ShouldProcess("console", "Showing start time")) { + Write-Message -Level Verbose -Message "Started: $dbStart." + } + + if ($sourceServer.VersionMajor -ge 9) { + $sourceDbOwnerChaining = $sourceServer.Databases[$dbName].DatabaseOwnershipChaining + $sourceDbTrustworthy = $sourceServer.Databases[$dbName].Trustworthy + $sourceDbBrokerEnabled = $sourceServer.Databases[$dbName].BrokerEnabled + } + + $sourceDbReadOnly = $sourceServer.Databases[$dbName].ReadOnly + + if ($SetSourceReadOnly) { + If ($Pscmdlet.ShouldProcess($source, "Set $dbName to read-only")) { + Write-Message -Level Verbose -Message "Setting database to read-only." + $result = Update-SqldbReadOnly -SqlInstance $sourceServer -dbname $dbName -readonly:$true + + if ($result -eq $false) { + Write-Message -Level Verbose -Message "Couldn't set database to read-only. Aborting routine for this database." + continue + } + } + } + + if ($BackupRestore) { + If ($Pscmdlet.ShouldProcess($destination, "Backup $dbName from $source and restoring.")) { + $copyDatabaseStatus.Type = "Database (BackupRestore)" + + $backupTmpResult = Backup-DbaDatabase -SqlInstance $sourceServer -Database $dbName -BackupDirectory $NetworkShare -FileCount $numberfiles -CopyOnly:$CopyOnly + $backupResult = $BackupTmpResult.BackupComplete + if ($backupResult -eq $false) { + $serviceAccount = $sourceServer.ServiceAccount + Write-Message -Level Verbose -Message "Backup Failed. Does SQL Server account $serviceAccount have access to $($NetworkShare)? Aborting routine for this database." + + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Backup failed. Verify service account access to $NetworkShare." + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + Write-Message -Level Verbose -Message "Reuse = $ReuseSourceFolderStructure." + $restoreResultTmp = $backupTmpResult | Restore-DbaDatabase -SqlInstance $destServer -DatabaseName $dbName -ReuseSourceFolderStructure:$ReuseSourceFolderStructure -NoRecovery:$NoRecovery -TrustDbBackupHistory -WithReplace:$WithReplace + $restoreResult = $restoreResultTmp.RestoreComplete + + if ($restoreResult -eq $true) { + Write-Message -Level Verbose -Message "Successfully restored $dbName to $destination." + $copyDatabaseStatus.Status = "Successful" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + else { + if ($ReuseSourceFolderStructure) { + Write-Message -Level Verbose -Message "Failed to restore $dbName to $destination. You specified -ReuseSourceFolderStructure. Does the exact same destination directory structure exist?" + Write-Message -Level Verbose -Message "Aborting routine for this database." + + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Failed to restore. ReuseSourceFolderStructure was specified, verify same directory structure exist on destination." + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + Write-Message -Level Verbose -Message "Failed to restore $dbName to $destination. Aborting routine for this database." + + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Failed to restore database" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + } + if ($NoBackupCleanUp -ne $true) { + foreach ($backupFile in ($restoreResultTmp.BackupFile -split ',')) { + try { + if (Test-Path $backupFile -ErrorAction Stop) { + Write-Message -Level Verbose -Message "Deleting $backupFile." + Remove-Item $backupFile -ErrorAction Stop + } + } + catch { + try { + Write-Message -Level Verbose -Message "Trying alternate SQL method to delete $backupFile." + $sql = "EXEC master.sys.xp_delete_file 0, '$backupFile'" + Write-Message -Level Debug -Message $sql + $null = $sourceServer.Query($sql) + } + catch { + Write-Message -Level Verbose -Message "Cannot delete backup file $backupFile." + + # Set NoBackupCleanup so that there's a warning at the end + $NoBackupCleanup = $true + } + } + } + } + } + + $dbFinish = Get-Date + if ($NoRecovery -eq $false) { + # needed because the newly restored database doesn't show up + $destServer.Databases.Refresh() + $dbOwner = $sourceServer.Databases[$dbName].Owner + if ($null -eq $dbOwner -or $destServer.Logins.Name -notcontains $dbOwner) { + $dbOwner = Get-SaLoginName -SqlInstance $destServer + } + Write-Message -Level Verbose -Message "Updating database owner to $dbOwner." + $OwnerResult = Set-DbaDatabaseOwner -SqlInstance $destServer -Database $dbName -TargetLogin $dbOwner -EnableException + if ($OwnerResult.Length -eq 0) { + Write-Message -Level Verbose -Message "Failed to update database owner." + } + } + } + + if ($DetachAttach) { + $copyDatabaseStatus.Type = "Database (DetachAttach)" + + $sourceFileStructure = New-Object System.Collections.Specialized.StringCollection + foreach ($file in $fileStructure.Databases[$dbName].Source.Values) { + $null = $sourceFileStructure.Add($file.Physical) + } + + $dbOwner = $sourceServer.Databases[$dbName].Owner + + if ($null -eq $dbOwner -or $destServer.Logins.Name -notcontains $dbOwner) { + $dbOwner = Get-SaLoginName -SqlInstance $destServer + } + + if ($Pscmdlet.ShouldProcess($destination, "Detach $dbName from $source and attach, then update dbowner")) { + $migrationResult = Start-SqlDetachAttach $sourceServer $destServer $fileStructure $dbName + + $dbFinish = Get-Date + + if ($reattach -eq $true) { + $sourceServer.Databases.Refresh() + $destServer.Databases.Refresh() + $result = Mount-SqlDatabase $sourceServer $dbName $sourceFileStructure $dbOwner + + if ($result -eq $true) { + $sourceServer.Databases[$dbName].DatabaseOwnershipChaining = $sourceDbOwnerChaining + $sourceServer.Databases[$dbName].Trustworthy = $sourceDbTrustworthy + $sourceServer.Databases[$dbName].BrokerEnabled = $sourceDbBrokerEnabled + $sourceServer.Databases[$dbName].Alter() + + if ($SetSourceReadOnly) { + $null = Update-SqldbReadOnly -SqlInstance $sourceServer -dbname $dbName -readonly $true + } + else { + $null = Update-SqldbReadOnly -SqlInstance $sourceServer -dbname $dbName -readonly $sourceDbReadOnly + } + + Write-Message -Level Verbose -Message "Successfully reattached $dbName to $source." + } + else { + Write-Message -Level Verbose -Message "Could not reattach $dbName to $source." + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Could not reattach database to $source" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + if ($migrationResult -eq $true) { + Write-Message -Level Verbose -Message "Successfully attached $dbName to $destination." + $copyDatabaseStatus.Status = "Successful" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + else { + Write-Message -Level Verbose -Message "Failed to attach $dbName to $destination. Aborting routine for this database." + + $copyDatabaseStatus.Status = "Failed" + $copyDatabaseStatus.Notes = "Failed to attach database to destination" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + continue + } + } + } + $destServer.Databases.Refresh() + + # restore potentially lost settings + if ($destServer.VersionMajor -ge 9 -and $NoRecovery -eq $false) { + if ($sourceDbOwnerChaining -ne $destServer.Databases[$dbName].DatabaseOwnershipChaining) { + if ($Pscmdlet.ShouldProcess($destination, "Updating DatabaseOwnershipChaining on $dbName")) { + try { + $destServer.Databases[$dbName].DatabaseOwnershipChaining = $sourceDbOwnerChaining + $destServer.Databases[$dbName].Alter() + Write-Message -Level Verbose -Message "Successfully updated DatabaseOwnershipChaining for $sourceDbOwnerChaining on $dbName on $destination." + } + catch { + $copyDatabaseStatus.Status = "Successful - failed to apply DatabaseOwnershipChaining." + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Failed to update DatabaseOwnershipChaining for $sourceDbOwnerChaining on $dbName on $destination." -Target $destination -ErrorRecord $_ -Continue + } + } + } + + if ($sourceDbTrustworthy -ne $destServer.Databases[$dbName].Trustworthy) { + if ($Pscmdlet.ShouldProcess($destination, "Updating Trustworthy on $dbName")) { + try { + $destServer.Databases[$dbName].Trustworthy = $sourceDbTrustworthy + $destServer.Databases[$dbName].Alter() + Write-Message -Level Verbose -Message "Successfully updated Trustworthy to $sourceDbTrustworthy for $dbName on $destination" + } + catch { + $copyDatabaseStatus.Status = "Successful - failed to apply Trustworthy" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Failed to update Trustworthy to $sourceDbTrustworthy for $dbName on $destination." -Target $destination -ErrorRecord $_ -Continue + } + } + } + + if ($sourceDbBrokerEnabled -ne $destServer.Databases[$dbName].BrokerEnabled) { + if ($Pscmdlet.ShouldProcess($destination, "Updating BrokerEnabled on $dbName")) { + try { + $destServer.Databases[$dbName].BrokerEnabled = $sourceDbBrokerEnabled + $destServer.Databases[$dbName].Alter() + Write-Message -Level Verbose -Message "Successfully updated BrokerEnabled to $sourceDbBrokerEnabled for $dbName on $destination." + } + catch { + $copyDatabaseStatus.Status = "Successful - failed to apply BrokerEnabled" + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Failed to update BrokerEnabled to $sourceDbBrokerEnabled for $dbName on $destination." -Target $destination -ErrorRecord $_ -Continue + } + } + } + } + + if ($sourceDbReadOnly -ne $destServer.Databases[$dbName].ReadOnly -and $NoRecovery -eq $false) { + if ($Pscmdlet.ShouldProcess($destination, "Updating ReadOnly status on $dbName")) { + $update = Update-SqldbReadOnly -SqlInstance $destServer -dbname $dbName -readonly $sourceDbReadOnly + if ($update -eq $true) { + Write-Message -Level Verbose -Message "Successfully updated readonly status on $dbName." + } + else { + $copyDatabaseStatus.Status = "Successful - failed to apply ReadOnly." + $copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Failed to update ReadOnly status on $dbName." -Target $destination -ErrorRecord $_ -Continue + } + } + } + + if ($SetSourceOffline -and $sourceServer.databases[$dbName].status -notlike '*offline*') { + if ($Pscmdlet.ShouldProcess($destination, "Setting $dbName offline on $source")) { + Stop-DbaProcess -SqlInstance $sourceServer -Database $dbName + Set-DbaDatabaseState -SqlInstance $sourceServer -SqlCredential $SourceSqlCredential -database $dbName -Offline + } + } + + if ($Pscmdlet.ShouldProcess("console", "Showing elapsed time")) { + $dbTotalTime = $dbFinish - $dbStart + $dbTotalTime = ($dbTotalTime.ToString().Split(".")[0]) + + Write-Message -Level Verbose -Message "Finished: $dbFinish." + Write-Message -Level Verbose -Message "Elapsed time: $dbTotalTime." + } + } # end db by db processing + } + } + end { + if (Test-FunctionInterrupt) { return } + + if ($Pscmdlet.ShouldProcess("console", "Showing migration time elapsed")) { + if ($null -ne $elapsed) { + + $totalTime = ($elapsed.Elapsed.toString().Split(".")[0]) + + Write-Message -Level Verbose -Message "`nDatabase migration finished" + Write-Message -Level Verbose -Message "Migration started: $started" + Write-Message -Level Verbose -Message "Migration completed: $(Get-Date)" + Write-Message -Level Verbose -Message "Total Elapsed time: $totalTime" + + if ($NetworkShare.length -gt 0 -and $NoBackupCleanup) { + Write-Message -Level Verbose -Message "Backups still exist at $NetworkShare." + } + } + else { + Write-Message -Level Verbose -Message "No work was done, as we stopped during setup phase" + } + } + } +} +function Copy-DbaDatabaseAssembly { + <# + .SYNOPSIS + Copy-DbaDatabaseAssembly migrates assemblies from one SQL Server to another. + + .DESCRIPTION + By default, all assemblies are copied. + + If the assembly already exists on the destination, it will be skipped unless -Force is used. + + This script does not yet copy dependencies or dependent objects. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Assembly + The assembly(ies) to process. This list is auto-populated from the server. If unspecified, all assemblies will be processed. + + .PARAMETER ExcludeAssembly + The assembly(ies) to exclude. This list is auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing assemblies on Destination with matching names from Source will be dropped. + + .NOTES + Tags: Migration, Assembly + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + http://dbatools.io/Get-SqlDatabaseAssembly + + .EXAMPLE + Copy-DbaDatabaseAssembly -Source sqlserver2014a -Destination sqlcluster + + Copies all assemblies from sqlserver2014a to sqlcluster using Windows credentials. If assemblies with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaDatabaseAssembly -Source sqlserver2014a -Destination sqlcluster -Assembly dbname.assemblyname, dbname3.anotherassembly -SourceSqlCredential $cred -Force + + Copies two assemblies, the dbname.assemblyname and dbname3.anotherassembly from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If an assembly with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + In this example, anotherassembly will be copied to the dbname3 database on the server sqlcluster. + + .EXAMPLE + Copy-DbaThing -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$Assembly, + [object[]]$ExcludeAssembly, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Assemblies are only supported in SQL Server 2005 and newer. Quitting." + } + } + process { + + $sourceAssemblies = @() + foreach ($database in ($sourceServer.Databases | Where-Object IsAccessible)) { + Write-Message -Level Verbose -Message "Processing $database on source" + + try { + # a bug here requires a try/catch + $userAssemblies = $database.Assemblies | Where-Object IsSystemObject -eq $false + foreach ($assembly in $userAssemblies) { + $sourceAssemblies += $assembly + } + } + catch { } + } + + $destAssemblies = @() + foreach ($database in $destServer.Databases) { + Write-Message -Level VeryVerbose -Message "Processing $database on destination" + try { + # a bug here requires a try/catch + $userAssemblies = $database.Assemblies | Where-Object IsSystemObject -eq $false + foreach ($assembly in $userAssemblies) { + $destAssemblies += $assembly + } + } + catch { } + } + foreach ($currentAssembly in $sourceAssemblies) { + $assemblyName = $currentAssembly.Name + $dbName = $currentAssembly.Parent.Name + $destDb = $destServer.Databases[$dbName] + Write-Message -Level VeryVerbose -Message "Processing $assemblyName on $dbname" + $copyDbAssemblyStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + SourceDatabase = $dbName + DestinationServer = $destServer.Name + DestinationDatabase = $destDb + type = "Database Assembly" + Name = $assemblyName + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + + if (!$destDb) { + $copyDbAssemblyStatus.Status = "Skipped" + $copyDbAssemblyStatus.Notes = "Destination database does not exist" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Destination database $dbName does not exist. Skipping $assemblyName."; + continue + } + + if ((Test-Bound -ParameterName Assembly) -and $Assembly -notcontains "$dbName.$assemblyName" -or $ExcludeAssembly -contains "$dbName.$assemblyName") { + continue + } + + if ($currentAssembly.AssemblySecurityLevel -eq "External" -and -not $destDb.Trustworthy) { + if ($Pscmdlet.ShouldProcess($destination, "Setting $dbName to External")) { + Write-Message -Level Verbose -Message "Setting $dbName Security Level to External on $destination." + $sql = "ALTER DATABASE $dbName SET TRUSTWORTHY ON" + try { + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + } + catch { + $copyDbAssemblyStatus.Status = "Failed" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue setting security level." -Target $destDb -ErrorRecord $_ + } + } + } + + if ($destServer.Databases[$dbName].Assemblies.Name -contains $currentAssembly.name) { + if ($force -eq $false) { + $copyDbAssemblyStatus.Status = "Skipped" + $copyDbAssemblyStatus.Notes = "Already exists" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Assembly $assemblyName exists at destination in the $dbName database. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping assembly $assemblyName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping assembly $assemblyName." + Write-Message -Level Verbose -Message "This won't work if there are dependencies." + $destServer.Databases[$dbName].Assemblies[$assemblyName].Drop() + Write-Message -Level Verbose -Message "Copying assembly $assemblyName." + $sql = $currentAssembly.Script() + Write-Message -Level Debug -Message $sql + $destServer.Query($sql, $dbName) + } + catch { + $copyDbAssemblyStatus.Status = "Failed" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping assembly." -Target $assemblyName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating assembly $assemblyName")) { + try { + Write-Message -Level Verbose -Message "Copying assembly $assemblyName from database." + $sql = $currentAssembly.Script() + Write-Message -Level Debug -Message $sql + $destServer.Query($sql, $dbName) + + $copyDbAssemblyStatus.Status = "Successful" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + catch { + $copyDbAssemblyStatus.Status = "Failed" + $copyDbAssemblyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating assembly." -Target $assemblyName -ErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlDatabaseAssembly + } +} +function Copy-DbaDatabaseMail { + <# + .SYNOPSIS + Migrates Mail Profiles, Accounts, Mail Servers and Mail Server Configs from one SQL Server to another. + + .DESCRIPTION + By default, all mail configurations for Profiles, Accounts, Mail Servers and Configs are copied. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Type + Specifies the object type to migrate. Valid options are "Job", "Alert" and "Operator". When Type is specified, all categories from the selected type will be migrated. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing objects on Destination with matching names from Source will be dropped. + + .NOTES + Tags: Migration, Mail + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaDatabaseMail + + .EXAMPLE + Copy-DbaDatabaseMail -Source sqlserver2014a -Destination sqlcluster + + Copies all database mail objects from sqlserver2014a to sqlcluster using Windows credentials. If database mail objects with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaDatabaseMail -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all database mail objects from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. + + .EXAMPLE + Copy-DbaDatabaseMail -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + + .EXAMPLE + Copy-DbaDatabaseMail -Source sqlserver2014a -Destination sqlcluster -EnableException + + Performs execution of function, and will throw a terminating exception if something breaks + #> + [cmdletbinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [Parameter(ParameterSetName = 'SpecificTypes')] + [ValidateSet('ConfigurationValues', 'Profiles', 'Accounts', 'mailServers')] + [string[]]$Type, + [PSCredential]$SourceSqlCredential, + [PSCredential]$DestinationSqlCredential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + function Copy-DbaDatabaseMailConfig { + [cmdletbinding(SupportsShouldProcess = $true)] + param () + + Write-Message -Message "Migrating mail server configuration values." -Level Verbose + $copyMailConfigStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = "Server Configuration" + Type = "Mail Configuration" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + if ($pscmdlet.ShouldProcess($destination, "Migrating all mail server configuration values.")) { + try { + $sql = $mail.ConfigurationValues.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + Write-Message -Message $sql -Level Debug + $destServer.Query($sql) | Out-Null + $mail.ConfigurationValues.Refresh() + $copyMailConfigStatus.Status = "Successful" + } + catch { + $copyMailConfigStatus.Status = "Failed" + $copyMailConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Unable to migrate mail configuration." -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer + } + } + $copyMailConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + + function Copy-DbaDatabaseAccount { + $sourceAccounts = $sourceServer.Mail.Accounts + $destAccounts = $destServer.Mail.Accounts + + Write-Message -Message "Migrating accounts." -Level Verbose + foreach ($account in $sourceAccounts) { + $accountName = $account.name + $copyMailAccountStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $accountName + Type = "Mail Account" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($accounts.count -gt 0 -and $accounts -notcontains $accountName) { + continue + } + + if ($destAccounts.name -contains $accountName) { + if ($force -eq $false) { + $copyMailAccountStatus.Status = "Skipped" + $copyMailAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Message "Account $accountName exists at destination. Use -Force to drop and migrate." -Level Verbose + continue + } + + If ($pscmdlet.ShouldProcess($destination, "Dropping account $accountName and recreating.")) { + try { + Write-Message -Message "Dropping account $accountName." -Level Verbose + $destServer.Mail.Accounts[$accountName].Drop() + $destServer.Mail.Accounts.Refresh() + } + catch { + $copyMailAccountStatus.Status = "Failed" + $copyMailAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping account." -Target $accountName -Category InvalidOperation -InnerErrorRecord $_ -Continue + } + } + } + + if ($pscmdlet.ShouldProcess($destination, "Migrating account $accountName.")) { + try { + Write-Message -Message "Copying mail account $accountName." -Level Verbose + $sql = $account.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + Write-Message -Message $sql -Level Debug + $destServer.Query($sql) | Out-Null + $copyMailAccountStatus.Status = "Successful" + } + catch { + $copyMailAccountStatus.Status = "Failed" + $copyMailAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying mail account." -Target $accountName -Category InvalidOperation -InnerErrorRecord $_ + } + } + $copyMailAccountStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + function Copy-DbaDatabaseMailProfile { + + $sourceProfiles = $sourceServer.Mail.Profiles + $destProfiles = $destServer.Mail.Profiles + + Write-Message -Message "Migrating mail profiles." -Level Verbose + foreach ($profile in $sourceProfiles) { + + $profileName = $profile.name + $copyMailProfileStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $profileName + Type = "Mail Profile" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($profiles.count -gt 0 -and $profiles -notcontains $profileName) { + continue + } + + if ($destProfiles.name -contains $profileName) { + if ($force -eq $false) { + $copyMailProfileStatus.Status = "Skipped" + $copyMailProfileStatus.Notes = "Already exists" + $copyMailProfileStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Message "Profile $profileName exists at destination. Use -Force to drop and migrate." -Level Verbose + continue + } + + If ($pscmdlet.ShouldProcess($destination, "Dropping profile $profileName and recreating.")) { + try { + Write-Message -Message "Dropping profile $profileName." -Level Verbose + $destServer.Mail.Profiles[$profileName].Drop() + $destServer.Mail.Profiles.Refresh() + } + catch { + $copyMailProfileStatus.Status = "Failed" + $copyMailProfileStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping profile." -Target $profileName -Category InvalidOperation -InnerErrorRecord $_ -Continue + } + } + } + + if ($pscmdlet.ShouldProcess($destination, "Migrating mail profile $profileName.")) { + try { + Write-Message -Message "Copying mail profile $profileName." -Level Verbose + $sql = $profile.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + Write-Message -Message $sql -Level Debug + $destServer.Query($sql) | Out-Null + $destServer.Mail.Profiles.Refresh() + $copyMailProfileStatus.Status = "Successful" + } + catch { + $copyMailProfileStatus.Status = "Failed" + $copyMailProfileStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying mail profile." -Target $profileName -Category InvalidOperation -InnerErrorRecord $_ + } + } + $copyMailProfileStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + function Copy-DbaDatabaseMailServer { + $sourceMailServers = $sourceServer.Mail.Accounts.MailServers + $destMailServers = $destServer.Mail.Accounts.MailServers + + Write-Message -Message "Migrating mail servers." -Level Verbose + foreach ($mailServer in $sourceMailServers) { + $mailServerName = $mailServer.name + $copyMailServerStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $mailServerName + Type = "Mail Server" + Status = $null + Notes = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + if ($mailServers.count -gt 0 -and $mailServers -notcontains $mailServerName) { + continue + } + + if ($destMailServers.name -contains $mailServerName) { + if ($force -eq $false) { + $copyMailServerStatus.Status = "Skipped" + $copyMailServerStatus.Notes = "Already exists" + $copyMailServerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Write-Message -Message "Mail server $mailServerName exists at destination. Use -Force to drop and migrate." -Level Verbose + continue + } + + If ($pscmdlet.ShouldProcess($destination, "Dropping mail server $mailServerName and recreating.")) { + try { + Write-Message -Message "Dropping mail server $mailServerName." -Level Verbose + $destServer.Mail.Accounts.MailServers[$mailServerName].Drop() + } + catch { + $copyMailServerStatus.Status = "Failed" + $copyMailServerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping mail server." -Target $mailServerName -Category InvalidOperation -InnerErrorRecord $_ -Continue + } + } + } + + if ($pscmdlet.ShouldProcess($destination, "Migrating account mail server $mailServerName.")) { + try { + Write-Message -Message "Copying mail server $mailServerName." -Level Verbose + $sql = $mailServer.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + Write-Message -Message $sql -Level Debug + $destServer.Query($sql) | Out-Null + $copyMailServerStatus.Status = "Successful" + } + catch { + $copyMailServerStatus.Status = "Failed" + $copyMailServerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue copying mail server" -Target $mailServerName -Category InvalidOperation -InnerErrorRecord $_ + } + } + $copyMailServerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + + if ($sourceServer.versionMajor -lt 9 -or $destServer.versionMajor -lt 9) { + Write-Message -Message "Database Mail is only supported in SQL Server 2005 and above. Quitting." -Level Verbose + } + + $mail = $sourceServer.mail + } + process { + + if ($type.count -gt 0) { + + switch ($type) { + "ConfigurationValues" { + Copy-DbaDatabaseMailConfig + $destServer.Mail.ConfigurationValues.Refresh() + } + + "Profiles" { + Copy-DbaDatabaseMailProfile + $destServer.Mail.Profiles.Refresh() + } + + "Accounts" { + Copy-DbaDatabaseAccount + $destServer.Mail.Accounts.Refresh() + } + + "mailServers" { + Copy-DbaDatabaseMailServer + } + } + + return + } + + if (($profiles.count + $accounts.count + $mailServers.count) -gt 0) { + + if ($profiles.count -gt 0) { + Copy-DbaDatabaseMailProfile -Profiles $profiles + $destServer.Mail.Profiles.Refresh() + } + + if ($accounts.count -gt 0) { + Copy-DbaDatabaseAccount -Accounts $accounts + $destServer.Mail.Accounts.Refresh() + } + + if ($mailServers.count -gt 0) { + Copy-DbaDatabaseMailServer -mailServers $mailServers + } + + return + } + + Copy-DbaDatabaseMailConfig + $destServer.Mail.ConfigurationValues.Refresh() + Copy-DbaDatabaseAccount + $destServer.Mail.Accounts.Refresh() + Copy-DbaDatabaseMailProfile + $destServer.Mail.Profiles.Refresh() + Copy-DbaDatabaseMailServer + $copyMailConfigStatus + $copyMailAccountStatus + $copyMailProfileStatus + $copyMailServerStatus + $enableDBMailStatus + + <# ToDo: Use Get/Set-DbaSpConfigure once the dynamic parameters are replaced. #> + + $sourceDbMailEnabled = ($sourceServer.Configuration.DatabaseMailEnabled).ConfigValue + Write-Message -Message "$sourceServer DBMail configuration value: $sourceDbMailEnabled." -Level Verbose + + $destDbMailEnabled = ($destServer.Configuration.DatabaseMailEnabled).ConfigValue + Write-Message -Message "$destServer DBMail configuration value: $destDbMailEnabled." -Level Verbose + $enableDBMailStatus = [pscustomobject]@{ + SourceServer = $sourceServer.name + DestinationServer = $destServer.name + Name = "Enabled on Destination" + Type = "Mail Configuration" + Status = if ($destDbMailEnabled -eq 1) { "Enabled" } else { $null } + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if (($sourceDbMailEnabled -eq 1) -and ($destDbMailEnabled -eq 0)) { + if ($pscmdlet.ShouldProcess($destination, "Enabling Database Mail")) { + try { + Write-Message -Message "Enabling Database Mail on $destServer." -Level Verbose + $destServer.Configuration.DatabaseMailEnabled.ConfigValue = 1 + $destServer.Alter() + $enableDBMailStatus.Status = "Successful" + } + catch { + $enableDBMailStatus.Status = "Failed" + $enableDBMailStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Cannot enable Database Mail." -Category InvalidOperation -InnerErrorRecord $_ -Target $destServer + } + } + $enableDBMailStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlDatabaseMail + } +} +function Copy-DbaEndpoint { + <# + .SYNOPSIS + Copy-DbaEndpoint migrates server endpoints from one SQL Server to another. + + .DESCRIPTION + By default, all endpoints are copied. + + If the endpoint already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Endpoint + The endpoint(s) to process. This list is auto-populated from the server. If unspecified, all endpoints will be processed. + + .PARAMETER ExcludeEndpoint + The endpoint(s) to exclude. This list is auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing endpoints on Destination with matching names from Source will be dropped. + + .NOTES + Tags: Migration, Endpoint + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaEndpoint + + .EXAMPLE + Copy-DbaEndpoint -Source sqlserver2014a -Destination sqlcluster + + Copies all server endpoints from sqlserver2014a to sqlcluster, using Windows credentials. If endpoints with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaEndpoint -Source sqlserver2014a -SourceSqlCredential $cred -Destination sqlcluster -Endpoint tg_noDbDrop -Force + + Copies only the tg_noDbDrop endpoint from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If an endpoint with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaEndpoint -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Endpoint, + [object[]]$ExcludeEndpoint, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + throw "Server Endpoints are only supported in SQL Server 2008 and above. Quitting." + } + } + process { + + $serverEndpoints = $sourceServer.Endpoints | Where-Object IsSystemObject -eq $false + $destEndpoints = $destServer.Endpoints + + foreach ($currentEndpoint in $serverEndpoints) { + $endpointName = $currentEndpoint.Name + + $copyEndpointStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $endpointName + Type = "Endpoint" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Endpoint -and $Endpoint -notcontains $endpointName -or $ExcludeEndpoint -contains $endpointName) { + continue + } + + if ($destEndpoints.Name -contains $endpointName) { + if ($force -eq $false) { + $copyEndpointStatus.Status = "Skipped" + $copyEndpointStatus.Notes = "Already exists" + $copyEndpointStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Server endpoint $endpointName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping server endpoint $endpointName and recreating.")) { + try { + Write-Message -Level Verbose -Message "Dropping server endpoint $endpointName." + $destServer.Endpoints[$endpointName].Drop() + } + catch { + $copyEndpointStatus.Status = "Failed" + $copyEndpointStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping server endpoint." -Target $endpointName -InnerErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating server endpoint $endpointName.")) { + try { + Write-Message -Level Verbose -Message "Copying server endpoint $endpointName." + $destServer.Query($currentEndpoint.Script()) | Out-Null + + $copyEndpointStatus.Status = "Successful" + $copyEndpointStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyEndpointStatus.Status = "Failed" + $copyEndpointStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating server endpoint." -Target $endpointName -InnerErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlEndpoint + } +} +function Copy-DbaExtendedEvent { + <# + .SYNOPSIS + Migrates SQL Extended Event Sessions except the two default sessions, AlwaysOn_health and system_health. + + .DESCRIPTION + Migrates SQL Extended Event Sessions except the two default sessions, AlwaysOn_health and system_health. + + By default, all non-system Extended Events are migrated. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER XeSession + The Extended Event Session(s) to process. This list is auto-populated from the server. If unspecified, all Extended Event Sessions will be processed. + + .PARAMETER ExcludeXeSession + The Extended Event Session(s) to exclude. This list is auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing Extended Events sessions on Destination with matching names from Source will be dropped. + + .NOTES + Tags: Migration, ExtendedEvent, XEvent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaExtendedEvent + + .EXAMPLE + Copy-DbaExtendedEvent -Source sqlserver2014a -Destination sqlcluster + + Copies all Extended Event sessions from sqlserver2014a to sqlcluster using Windows credentials. + + .EXAMPLE + Copy-DbaExtendedEvent -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all Extended Event sessions from sqlserver2014a to sqlcluster using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. + + .EXAMPLE + Copy-DbaExtendedEvent -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + + .EXAMPLE + Copy-DbaExtendedEvent -Source sqlserver2014a -Destination sqlcluster -XeSession CheckQueries, MonitorUserDefinedException + + Copies only the Extended Events named CheckQueries and MonitorUserDefinedException from sqlserver2014a to sqlcluster. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $SourceSqlCredential, + [PSCredential] + $DestinationSqlCredential, + [object[]]$XeSession, + [object[]]$ExcludeXeSession, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential -MinimumVersion 10 + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential -MinimumVersion 10 + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + } + process { + + $sourceSqlConn = $sourceServer.ConnectionContext.SqlConnectionObject + $sourceSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $sourceSqlConn + $sourceStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $sourceSqlStoreConnection + + $destSqlConn = $destServer.ConnectionContext.SqlConnectionObject + $destSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $destSqlConn + $destStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $destSqlStoreConnection + + $storeSessions = $sourceStore.Sessions | Where-Object { $_.Name -notin 'AlwaysOn_health', 'system_health' } + if ($XeSession) { + $storeSessions = $storeSessions | Where-Object Name -In $XeSession + } + if ($ExcludeXeSession) { + $storeSessions = $storeSessions | Where-Object Name -NotIn $ExcludeXeSession + } + + Write-Message -Level Verbose -Message "Migrating sessions." + foreach ($session in $storeSessions) { + $sessionName = $session.Name + + $copyXeSessionStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $sessionName + Type = "Extended Event" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destStore.Sessions[$sessionName]) { + if ($force -eq $false) { + $copyXeSessionStatus.Status = "Skipped" + $copyXeSessionStatus.Notes = "Already exists" + $copyXeSessionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "Extended Event Session '$sessionName' was skipped because it already exists on $destination." + Write-Message -Level Verbose -Message "Use -Force to drop and recreate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to drop $sessionName")) { + Write-Message -Level Verbose -Message "Extended Event Session '$sessionName' exists on $destination." + Write-Message -Level Verbose -Message "Force specified. Dropping $sessionName." + + try { + $destStore.Sessions[$sessionName].Drop() + } + catch { + $copyXeSessionStatus.Status = "Failed" + $copyXeSessionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Unable to drop session. Moving on." -Target $sessionName -InnerErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Migrating session $sessionName")) { + try { + $sql = $session.ScriptCreate().GetScript() | Out-String + + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Migrating session $sessionName." + $null = $destServer.Query($sql) + + if ($session.IsRunning -eq $true) { + $destStore.Sessions.Refresh() + $destStore.Sessions[$sessionName].Start() + } + + $copyXeSessionStatus.Status = "Successful" + $copyXeSessionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyXeSessionStatus.Status = "Failed" + $copyXeSessionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Unable to create session." -Target $sessionName -InnerErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlExtendedEvent + } +} +function Copy-DbaLinkedServer { + <# + .SYNOPSIS + Copy-DbaLinkedServer migrates Linked Servers from one SQL Server to another. Linked Server logins and passwords are migrated as well. + + .DESCRIPTION + By using password decryption techniques provided by Antti Rantasaari (NetSPI, 2014), this script migrates SQL Server Linked Servers from one server to another, while maintaining username and password. + + Credit: https://blog.netspi.com/decrypting-mssql-database-link-server-passwords/ + License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause + + .PARAMETER Source + Source SQL Server (2005 and above). You must have sysadmin access to both SQL Server and Windows. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server (2005 and above). You must have sysadmin access to both SQL Server and Windows. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER LinkedServer + The linked server(s) to process - this list is auto-populated from the server. If unspecified, all linked servers will be processed. + + .PARAMETER ExcludeLinkedServer + The linked server(s) to exclude - this list is auto-populated from the server + + .PARAMETER UpgradeSqlClient + Upgrade any SqlClient Linked Server to the current Version + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + By default, if a Linked Server exists on the source and destination, the Linked Server is not copied over. Specifying -force will drop and recreate the Linked Server on the Destination server. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: WSMan, Migration, LinkedServer + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers, Remote Registry & Remote Administration enabled and accessible on source server. + + Limitations: Hasn't been tested thoroughly. Works on Win8.1 and SQL Server 2012 & 2014 so far. + This just copies the SQL portion. It does not copy files (ie. a local SQLite database, or Microsoft Access DB), nor does it configure ODBC entries. + + .LINK + https://dbatools.io/Copy-DbaLinkedServer + + .EXAMPLE + Copy-DbaLinkedServer -Source sqlserver2014a -Destination sqlcluster + + Description + Copies all SQL Server Linked Servers on sqlserver2014a to sqlcluster. If Linked Server exists on destination, it will be skipped. + + .EXAMPLE + Copy-DbaLinkedServer -Source sqlserver2014a -Destination sqlcluster -LinkedServer SQL2K5,SQL2k -Force + + Description + Copies over two SQL Server Linked Servers (SQL2K and SQL2K2) from sqlserver to sqlcluster. If the credential already exists on the destination, it will be dropped. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$LinkedServer, + [object[]]$ExcludeLinkedServer, + [switch]$UpgradeSqlClient, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $null = Test-ElevationRequirement -ComputerName $Source.ComputerName + function Get-LinkedServerLogins { + <# + .SYNOPSIS + Internal function. + + This function is heavily based on Antti Rantasaari's script at http://goo.gl/wpqSib + Antti Rantasaari 2014, NetSPI + License: BSD 3-Clause http://opensource.org/licenses/BSD-3-Clause + #> + param ( + $SqlInstance + ) + + $server = $SqlInstance + $sourceName = $server.Name + + # Query Service Master Key from the database - remove padding from the key + # key_id 102 eq service master key, thumbprint 3 means encrypted with machinekey + $sql = "SELECT substring(crypt_property,9,len(crypt_property)-8) as smk FROM sys.key_encryptions WHERE key_id=102 and (thumbprint=0x03 or thumbprint=0x0300000001)" + try { + $smkbytes = $server.Query($sql).smk + } + catch { + Stop-Function -Message "Can't run query." -Target $server -InnerErrorRecord $_ + return + } + + $sourceNetBios = Resolve-NetBiosName $server + $instance = $server.InstanceName + $serviceInstanceId = $server.ServiceInstanceId + + # Get entropy from the registry - hopefully finds the right SQL server instance + try { + [byte[]]$entropy = Invoke-Command2 -Raw -Credential $Credential -ComputerName $sourceNetBios -argumentlist $serviceInstanceId { + $serviceInstanceId = $args[0] + $entropy = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$serviceInstanceId\Security\").Entropy + return $entropy + } + } + catch { + Stop-Function -Message "Can't access registry keys on $sourceName. Quitting." -Target $server ErrorRecord $_ + return + } + + # Decrypt the service master key + try { + $serviceKey = Invoke-Command2 -Raw -Credential $Credential -ComputerName $sourceNetBios -ArgumentList $smkbytes, $Entropy { + Add-Type -assembly System.Security + Add-Type -assembly System.Core + $smkbytes = $args[0]; $Entropy = $args[1] + $serviceKey = [System.Security.Cryptography.ProtectedData]::Unprotect($smkbytes, $Entropy, 'LocalMachine') + return $serviceKey + } + } + catch { + Stop-Function -Message "Can't unprotect registry data on $($source.Name)). Quitting." -Target $server -InnerErrorRecord $_ + return + } + + # Choose the encryption algorithm based on the SMK length - 3DES for 2008, AES for 2012 + # Choose IV length based on the algorithm + if (($serviceKey.Length -ne 16) -and ($serviceKey.Length -ne 32)) { + Write-Message -Level Verbose -Message "ServiceKey found: $serviceKey.Length" + Stop-Function -Message "Unknown key size. Cannot continue." -Target $source + return + + } + + if ($serviceKey.Length -eq 16) { + $decryptor = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider + $ivlen = 8 + } + elseif ($serviceKey.Length -eq 32) { + $decryptor = New-Object System.Security.Cryptography.AESCryptoServiceProvider + $ivlen = 16 + } + + <# + Query link server password information from the Db. + Remove header from pwdhash, extract IV (as iv) and ciphertext (as pass) + Ignore links with blank credentials (integrated auth ?) + #> + if ($server.IsClustered -eq $false) { + $connString = "Server=ADMIN:$sourceNetBios\$instance;Trusted_Connection=True" + } + else { + $dacEnabled = $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue + + if ($dacEnabled -eq $false) { + If ($Pscmdlet.ShouldProcess($server.Name, "Enabling DAC on clustered instance.")) { + Write-Message -Level Verbose -Message "DAC must be enabled for clusters, even when accessed from active node. Enabling." + $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue = $true + $server.Configuration.Alter() + } + } + + $connString = "Server=ADMIN:$sourceName;Trusted_Connection=True" + } + + <# NOTE: This query is accessing syslnklgns table. Can only be done via the DAC connection #> + $sql = " + SELECT sysservers.srvname, + syslnklgns.Name, + substring(syslnklgns.pwdhash,5,$ivlen) iv, + substring(syslnklgns.pwdhash,$($ivlen + 5), + len(syslnklgns.pwdhash)-$($ivlen + 4)) pass + FROM master.sys.syslnklgns + inner join master.sys.sysservers + on syslnklgns.srvid=sysservers.srvid + WHERE len(pwdhash) > 0" + + # Get entropy from the registry + try { + $logins = Invoke-Command2 -Raw -Credential $Credential -ComputerName $sourceNetBios -ArgumentList $connString, $sql { + $connString = $args[0]; $sql = $args[1] + $conn = New-Object System.Data.SqlClient.SQLConnection($connString) + $conn.open() + $cmd = New-Object System.Data.SqlClient.SqlCommand($sql, $conn); + $data = $cmd.ExecuteReader() + $dt = New-Object "System.Data.DataTable" + $dt.Load($data) + $conn.Close() + $conn.Dispose() + return $dt + } + } + catch { + Stop-Function -Message "Can't establish local DAC connection." -Target $server -InnerErrorRecord $_ + return + } + + if ($server.IsClustered -and $dacEnabled -eq $false) { + If ($Pscmdlet.ShouldProcess($server.Name, "Disabling DAC on clustered instance.")) { + Write-Message -Level Verbose -Message "Setting DAC config back to 0." + $server.Configuration.RemoteDacConnectionsEnabled.ConfigValue = $false + $server.Configuration.Alter() + } + } + + $decryptedLogins = New-Object "System.Data.DataTable" + [void]$decryptedLogins.Columns.Add("LinkedServer") + [void]$decryptedLogins.Columns.Add("Login") + [void]$decryptedLogins.Columns.Add("Password") + + + # Go through each row in results + foreach ($login in $logins) { + # decrypt the password using the service master key and the extracted IV + $decryptor.Padding = "None" + $decrypt = $decryptor.Createdecryptor($serviceKey, $login.iv) + $stream = New-Object System.IO.MemoryStream ( , $login.pass) + $crypto = New-Object System.Security.Cryptography.CryptoStream $stream, $decrypt, "Write" + + $crypto.Write($login.pass, 0, $login.pass.Length) + [byte[]]$decrypted = $stream.ToArray() + + # convert decrypted password to unicode + $encode = New-Object System.Text.UnicodeEncoding + + # Print results - removing the weird padding (8 bytes in the front, some bytes at the end)... + # Might cause problems but so far seems to work.. may be dependant on SQL server version... + # If problems arise remove the next three lines.. + $i = 8; foreach ($b in $decrypted) {if ($decrypted[$i] -ne 0 -and $decrypted[$i + 1] -ne 0 -or $i -eq $decrypted.Length) { $i -= 1; break; }; $i += 1; } + $decrypted = $decrypted[8..$i] + + [void]$decryptedLogins.Rows.Add($($login.srvname), $($login.Name), $($encode.GetString($decrypted))) + } + return $decryptedLogins + } + + function Copy-DbaLinkedServers { + param ( + [string[]]$LinkedServer, + [bool]$force + ) + + Write-Message -Level Verbose -Message "Collecting Linked Server logins and passwords on $($sourceServer.Name)." + $sourcelogins = Get-LinkedServerLogins $sourceServer + + $serverlist = $sourceServer.LinkedServers + + if ($LinkedServer) { + $serverlist = $serverlist | Where-Object Name -In $LinkedServer + } + if ($ExcludeLinkedServer) { + $serverList = $serverlist | Where-Object Name -NotIn $ExcludeLinkedServer + } + + foreach ($currentLinkedServer in $serverlist) { + $provider = $currentLinkedServer.ProviderName + try { + $destServer.LinkedServers.Refresh() + $destServer.LinkedServers.LinkedServerLogins.Refresh() + } + catch { } + + $linkedServerName = $currentLinkedServer.Name + + $copyLinkedServer = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $linkedServerName + Type = "Linked Server" + Status = $null + Notes = $provider + DateTime = [DbaDateTime](Get-Date) + } + + # This does a check to warn of missing OleDbProviderSettings but should only be checked on SQL on Windows + if ($destServer.Settings.OleDbProviderSettings.Name.Length -ne 0) { + if (!$destServer.Settings.OleDbProviderSettings.Name -contains $provider -and !$provider.StartsWith("SQLN")) { + $copyLinkedServer.Status = "Skipped" + $copyLinkedServer.Notes = "Missing provider" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "$($destServer.Name) does not support the $provider provider. Skipping $linkedServerName." + continue + } + } + + if ($null -ne $destServer.LinkedServers[$linkedServerName]) { + if (!$force) { + $copyLinkedServer.Status = "Skipped" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Write-Message -Level Verbose -Message "$linkedServerName exists $($destServer.Name). Skipping." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping $linkedServerName")) { + if ($currentLinkedServer.Name -eq 'repl_distributor') { + Write-Message -Level Verbose -Message "repl_distributor cannot be dropped. Not going to try." + continue + } + + $destServer.LinkedServers[$linkedServerName].Drop($true) + $destServer.LinkedServers.refresh() + } + } + } + + Write-Message -Level Verbose -Message "Attempting to migrate: $linkedServerName." + If ($Pscmdlet.ShouldProcess($destination, "Migrating $linkedServerName")) { + try { + $sql = $currentLinkedServer.Script() | Out-String + Write-Message -Level Debug -Message $sql + + if ($UpgradeSqlClient -and $sql -match "sqlncli") { + $destProviders = $destServer.Settings.OleDbProviderSettings | Where-Object { $_.Name -like 'SQLNCLI*' } + $newProvider = $destProviders | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name + + Write-Message -Level Verbose -Message "Changing sqlncli to $newProvider" + $sql = $sql -replace ("sqlncli[0-9]+", $newProvider) + } + + $destServer.Query($sql) + $destServer.LinkedServers.Refresh() + Write-Message -Level Verbose -Message "$linkedServerName successfully copied." + + $copyLinkedServer.Status = "Successful" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyLinkedServer.Status = "Failed" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue adding linked server $destServer." -Target $linkedServerName -InnerErrorRecord $_ + $skiplogins = $true + } + } + + if ($skiplogins -ne $true) { + $destlogins = $destServer.LinkedServers[$linkedServerName].LinkedServerLogins + $lslogins = $sourcelogins | Where-Object { $_.LinkedServer -eq $linkedServerName } + + foreach ($login in $lslogins) { + if ($Pscmdlet.ShouldProcess($destination, "Migrating $($login.Login)")) { + $currentlogin = $destlogins | Where-Object { $_.RemoteUser -eq $login.Login } + + $copyLinkedServer.Type = $login.Login + + if ($currentlogin.RemoteUser.length -ne 0) { + try { + $currentlogin.SetRemotePassword($login.Password) + $currentlogin.Alter() + + $copyLinkedServer.Status = "Successful" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyLinkedServer.Status = "Failed" + $copyLinkedServer | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Failed to copy login." -Target $login -InnerErrorRecord $_ + } + } + } + } + } + } + } + } + process { + if (Test-FunctionInterrupt) { return } + if ($null -ne $SourceSqlCredential.username) { + Write-Message -Level Verbose -Message "You are using a SQL Credential. Note that this script requires Windows Administrator access on the source server. Attempting with $($SourceSqlCredential.Username)." + } + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.Name + $destination = $destServer.Name + + if (!(Test-SqlSa -SqlInstance $sourceServer -SqlCredential $SourceSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $source. Quitting." -Target $sourceServer + return + } + if (!(Test-SqlSa -SqlInstance $destServer -SqlCredential $DestinationSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $destination. Quitting." -Target $destServer + return + } + + Write-Message -Level Verbose -Message "Getting NetBios name for $source." + $sourceNetBios = Resolve-NetBiosName $sourceserver + + Write-Message -Level Verbose -Message "Checking if Remote Registry is enabled on $source." + try { + Invoke-Command2 -Raw -Credential $Credential -ComputerName $sourceNetBios -ScriptBlock { Get-ItemProperty -Path "HKLM:\SOFTWARE\" } -ErrorAction Stop + } + catch { + Stop-Function -Message "Can't connect to registry on $source." -Target $sourceNetBios -ErrorRecord $_ + return + } + + # Magic happens here + Copy-DbaLinkedServers $LinkedServer -Force:$force + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlLinkedServer + } +} +function Copy-DbaLogin { + <# + .SYNOPSIS + Migrates logins from source to destination SQL Servers. Supports SQL Server versions 2000 and newer. + + .DESCRIPTION + SQL Server 2000: Migrates logins with SIDs, passwords, server roles and database roles. + + SQL Server 2005 & newer: Migrates logins with SIDs, passwords, defaultdb, server roles & securables, database permissions & securables, login attributes (enforce password policy, expiration, etc.) + + The login hash algorithm changed in SQL Server 2012, and is not backwards compatible with previous SQL Server versions. This means that while SQL Server 2000 logins can be migrated to SQL Server 2012, logins created in SQL Server 2012 can only be migrated to SQL Server 2012 and above. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Login + The login(s) to process. Options for this list are auto-populated from the server. If unspecified, all logins will be processed. + + .PARAMETER ExcludeLogin + The login(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER ExcludeSystemLogin + If this switch is enabled, NT SERVICE accounts will be skipped. + + .PARAMETER SyncOnly + If this switch is enabled, only SQL Server login permissions, roles, etc. will be synced. Logins and users will not be added or dropped. If a matching Login does not exist on the destination, the Login will be skipped. + Credential removal is not currently supported for this parameter. + + .PARAMETER SyncSaName + If this switch is enabled, the name of the sa account will be synced between Source and Destination + + .PARAMETER OutFile + Calls Export-SqlLogin and exports all logins to a T-SQL formatted file. This does not perform a copy, so no destination is required. + + .PARAMETER InputObject + Takes the parameters required from a Login object that has been piped into the command + + .PARAMETER LoginRenameHashtable + Pass a hash table into this parameter to be passed into Rename-DbaLogin to update the Login and mappings after the Login is completed. + + .PARAMETER KillActiveConnection + If this switch and -Force are enabled, all active connections and sessions on Destination will be killed. + + A login cannot be dropped when it has active connections on the instance. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Login(s) will be dropped and recreated on Destination. Logins that own Agent jobs cannot be dropped at this time. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Login + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaLogin + + .EXAMPLE + Copy-DbaLogin -Source sqlserver2014a -Destination sqlcluster -Force + + Copies all logins from Source Destination. If a SQL Login on Source exists on the Destination, the Login on Destination will be dropped and recreated. + + If active connections are found for a login, the copy of that Login will fail as it cannot be dropped. + + .EXAMPLE + Copy-DbaLogin -Source sqlserver2014a -Destination sqlcluster -Force -KillActiveConnection + + Copies all logins from Source Destination. If a SQL Login on Source exists on the Destination, the Login on Destination will be dropped and recreated. + + If any active connections are found they will be killed. + + .EXAMPLE + Copy-DbaLogin -Source sqlserver2014a -Destination sqlcluster -Exclude realcajun -SourceSqlCredential $scred -DestinationSqlCredential $dcred + + Copies all Logins from Source to Destination except for realcajun using SQL Authentication to connect to both instances. + + If a Login already exists on the destination, it will not be migrated. + + .EXAMPLE + Copy-DbaLogin -Source sqlserver2014a -Destination sqlcluster -Login realcajun, netnerds -force + + Copies ONLY Logins netnerds and realcajun. If Login realcajun or netnerds exists on Destination, the existing Login(s) will be dropped and recreated. + + .EXAMPLE + Copy-DbaLogin -Source sqlserver2014a -Destination sqlcluster -SyncOnly + + Syncs only SQL Server login permissions, roles, etc. Does not add or drop logins or users. + + If a matching Login does not exist on Destination, the Login will be skipped. + + .EXAMPLE + Copy-DbaLogin -LoginRenameHashtable @{ "OldUser" ="newlogin" } -Source $Sql01 -Destination Localhost -SourceSqlCredential $sqlcred + + Copies OldUser and then renames it to newlogin. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 | Out-GridView -Passthru | Copy-DbaLogin -Destination sql2017 + + Displays all available logins on sql2016 in a grid view, then copies all selected logins to sql2017. + #> + + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess)] + Param ( + [parameter(ParameterSetName = "SqlInstance", Mandatory)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$Login, + [object[]]$ExcludeLogin, + [switch]$ExcludeSystemLogin, + [switch]$SyncOnly, + [parameter(ParameterSetName = "Live")] + [parameter(ParameterSetName = "SqlInstance")] + [switch]$SyncSaName, + [parameter(ParameterSetName = "File", Mandatory)] + [string]$OutFile, + [parameter(ParameterSetName = "InputObject", ValueFromPipeline)] + [object]$InputObject, + [hashtable]$LoginRenameHashtable, + [switch]$KillActiveConnection, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Copy-Login { + foreach ($sourceLogin in $sourceServer.Logins) { + + $userName = $sourceLogin.name + + $copyLoginStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Login - $($sourceLogin.LoginType)" + Name = $userName + DestinationLogin = $userName + SourceLogin = $userName + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Login -and $Login -notcontains $userName -or $ExcludeLogin -contains $userName) { continue } + + if ($sourceLogin.id -eq 1) { continue } + + if ($userName.StartsWith("##") -or $userName -eq 'sa') { + Write-Message -Level Verbose -Message "Skipping $userName." + continue + } + + $serverName = Resolve-NetBiosName $sourceServer + + $currentLogin = $sourceServer.ConnectionContext.truelogin + + if ($currentLogin -eq $userName -and $force) { + if ($Pscmdlet.ShouldProcess("console", "Stating $userName is skipped because it is performing the migration.")) { + Write-Message -Level Verbose -Message "Cannot drop login performing the migration. Skipping." + } + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "Current login" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if (($destServer.LoginMode -ne [Microsoft.SqlServer.Management.Smo.ServerLoginMode]::Mixed) -and ($sourceLogin.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin)) { + Write-Message -Level Verbose -Message "$Destination does not have Mixed Mode enabled. [$userName] is an SQL Login. Enable mixed mode authentication after the migration completes to use this type of login." + } + + $userBase = ($userName.Split("\")[0]).ToLower() + + if ($serverName -eq $userBase -or $userName.StartsWith("NT ")) { + if ($sourceServer.NetName -ne $destServer.NetName) { + if ($Pscmdlet.ShouldProcess("console", "Stating $userName was skipped because it is a local machine name.")) { + Write-Message -Level Verbose -Message "$userName was skipped because it is a local machine name." + } + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "Local machine name" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($ExcludeSystemLogin) { + Write-Message -Level Verbose -Message "$userName was skipped because ExcludeSystemLogin was specified." + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "System login" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($Pscmdlet.ShouldProcess("console", "Stating local login $userName since the source and destination server reside on the same machine.")) { + Write-Message -Level Verbose -Message "Copying local login $userName since the source and destination server reside on the same machine." + } + } + } + + if ($null -ne $destServer.Logins.Item($userName) -and !$force) { + if ($Pscmdlet.ShouldProcess("console", "Stating $userName is skipped because it exists at destination.")) { + Write-Message -Level Verbose -Message "$userName already exists in destination. Use -Force to drop and recreate." + } + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "Already exists" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($null -ne $destServer.Logins.Item($userName) -and $force) { + if ($userName -eq $destServer.ServiceAccount) { + Write-Message -Level Verbose -Message "$userName is the destination service account. Skipping drop." + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "Destination service account" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping $userName")) { + + # Kill connections, delete user + Write-Message -Level Verbose -Message "Attempting to migrate $userName" + Write-Message -Level Verbose -Message "Force was specified. Attempting to drop $userName on $destination." + + try { + $ownedDbs = $destServer.Databases | Where-Object Owner -eq $userName + + foreach ($ownedDb in $ownedDbs) { + Write-Message -Level Verbose -Message "Changing database owner for $($ownedDb.name) from $userName to sa." + $ownedDb.SetOwner('sa') + $ownedDb.Alter() + } + + $ownedJobs = $destServer.JobServer.Jobs | Where-Object OwnerLoginName -eq $userName + + foreach ($ownedJob in $ownedJobs) { + Write-Message -Level Verbose -Message "Changing job owner for $($ownedJob.name) from $userName to sa." + $ownedJob.Set_OwnerLoginName('sa') + $ownedJob.Alter() + } + + $activeConnections = $destServer.EnumProcesses() | Where-Object Login -eq $userName + + if ($activeConnections -and $KillActiveConnection) { + if (!$destServer.Logins.Item($userName).IsDisabled) { + $disabled = $true + $destServer.Logins.Item($userName).Disable() + } + + $activeConnections | ForEach-Object { $destServer.KillProcess($_.Spid)} + Write-Message -Level Verbose -Message "-KillActiveConnection was provided. There are $($activeConnections.Count) active connections killed." + # just in case the kill didn't work, it'll leave behind a disabled account + if ($disabled) { $destServer.Logins.Item($userName).Enable() } + } + elseif ($activeConnections) { + Write-Message -Level Verbose -Message "There are $($activeConnections.Count) active connections found for the login $userName. Utilize -KillActiveConnection with -Force to kill the connections." + } + $destServer.Logins.Item($userName).Drop() + + Write-Message -Level Verbose -Message "Successfully dropped $userName on $destination." + } + catch { + $copyLoginStatus.Status = "Failed" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Could not drop $userName." -Category InvalidOperation -ErrorRecord $_ -Target $destServer -Continue 3>$null + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Adding SQL login $userName")) { + + Write-Message -Level Verbose -Message "Attempting to add $userName to $destination." + $destLogin = New-Object Microsoft.SqlServer.Management.Smo.Login($destServer, $userName) + + Write-Message -Level Verbose -Message "Setting $userName SID to source username SID." + $destLogin.Set_Sid($sourceLogin.Get_Sid()) + + $defaultDb = $sourceLogin.DefaultDatabase + + Write-Message -Level Verbose -Message "Setting login language to $($sourceLogin.Language)." + $destLogin.Language = $sourceLogin.Language + + if ($null -eq $destServer.databases[$defaultDb]) { + # we end up here when the default database on source doesn't exist on dest + # if source login is a sysadmin, then set the default database to master + # if not, set it to tempdb (see #303) + $OrigdefaultDb = $defaultDb + try { $sourcesysadmins = $sourceServer.roles['sysadmin'].EnumMemberNames() } + catch { $sourcesysadmins = $sourceServer.roles['sysadmin'].EnumServerRoleMembers() } + if ($sourcesysadmins -contains $userName) { + $defaultDb = "master" + } + else { + $defaultDb = "tempdb" + } + Write-Message -Level Verbose -Message "$OrigdefaultDb does not exist on destination. Setting defaultdb to $defaultDb." + } + + Write-Message -Level Verbose -Message "Set $userName defaultdb to $defaultDb." + $destLogin.DefaultDatabase = $defaultDb + + $checkexpiration = "ON"; $checkpolicy = "ON" + + if ($sourceLogin.PasswordPolicyEnforced -eq $false) { $checkpolicy = "OFF" } + + if (!$sourceLogin.PasswordExpirationEnabled) { $checkexpiration = "OFF" } + + $destLogin.PasswordPolicyEnforced = $sourceLogin.PasswordPolicyEnforced + $destLogin.PasswordExpirationEnabled = $sourceLogin.PasswordExpirationEnabled + + # Attempt to add SQL Login User + if ($sourceLogin.LoginType -eq "SqlLogin") { + $destLogin.LoginType = "SqlLogin" + $sourceLoginname = $sourceLogin.name + + switch ($sourceServer.versionMajor) { + 0 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM master.dbo.syslogins WHERE loginname='$sourceLoginname'" } + 8 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM dbo.syslogins WHERE name='$sourceLoginname'" } + 9 { $sql = "SELECT CONVERT(VARBINARY(256),password_hash) as hashedpass FROM sys.sql_logins where name='$sourceLoginname'" } + default { + $sql = "SELECT CAST(CONVERT(VARCHAR(256), CAST(LOGINPROPERTY(name,'PasswordHash') + AS VARBINARY(256)), 1) AS NVARCHAR(max)) AS hashedpass FROM sys.server_principals + WHERE principal_id = $($sourceLogin.id)" + } + } + + try { + $hashedPass = $sourceServer.ConnectionContext.ExecuteScalar($sql) + } + catch { + $hashedPassDt = $sourceServer.Databases['master'].ExecuteWithResults($sql) + $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) + } + + if ($hashedPass.GetType().Name -ne "String") { + $passString = "0x"; $hashedPass | ForEach-Object { $passString += ("{0:X}" -f $_).PadLeft(2, "0") } + $hashedPass = $passString + } + + try { + $destLogin.Create($hashedPass, [Microsoft.SqlServer.Management.Smo.LoginCreateOptions]::IsHashed) + $destLogin.Refresh() + Write-Message -Level Verbose -Message "Successfully added $userName to $destination." + + $copyLoginStatus.Status = "Successful" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + catch { + try { + $sid = "0x"; $sourceLogin.sid | ForEach-Object { $sid += ("{0:X}" -f $_).PadLeft(2, "0") } + $sql = "CREATE LOGIN [$userName] WITH PASSWORD = $hashedPass HASHED, SID = $sid, + DEFAULT_DATABASE = [$defaultDb], CHECK_POLICY = $checkpolicy, + CHECK_EXPIRATION = $checkexpiration, DEFAULT_LANGUAGE = [$($sourceLogin.Language)]" + + $null = $destServer.Query($sql) + + $destLogin = $destServer.logins[$userName] + Write-Message -Level Verbose -Message "Successfully added $userName to $destination." + + $copyLoginStatus.Status = "Successful" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + catch { + $copyLoginStatus.Status = "Failed" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Failed to add $userName to $destination." -Category InvalidOperation -ErrorRecord $_ -Target $destServer -Continue 3>$null + } + } + } + # Attempt to add Windows User + elseif ($sourceLogin.LoginType -eq "WindowsUser" -or $sourceLogin.LoginType -eq "WindowsGroup") { + Write-Message -Level Verbose -Message "Adding as login type $($sourceLogin.LoginType)" + $destLogin.LoginType = $sourceLogin.LoginType + + Write-Message -Level Verbose -Message "Setting language as $($sourceLogin.Language)" + $destLogin.Language = $sourceLogin.Language + + try { + $destLogin.Create() + $destLogin.Refresh() + Write-Message -Level Verbose -Message "Successfully added $userName to $destination." + + $copyLoginStatus.Status = "Successful" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + catch { + $copyLoginStatus.Status = "Failed" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Failed to add $userName to $destination" -Category InvalidOperation -ErrorRecord $_ -Target $destServer -Continue 3>$null + } + } + # This script does not currently support certificate mapped or asymmetric key users. + else { + Write-Message -Level Verbose -Message "$($sourceLogin.LoginType) logins not supported. $($sourceLogin.name) skipped." + + $copyLoginStatus.Status = "Skipped" + $copyLoginStatus.Notes = "$($sourceLogin.LoginType) not supported" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + continue + } + + if ($sourceLogin.IsDisabled) { + try { + $destLogin.Disable() + } + catch { + $copyLoginStatus.Status = "Successful - but could not disable on destination" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "$userName disabled on source, could not be disabled on $destination." -Category InvalidOperation -ErrorRecord $_ -Target $destServer 3>$null + } + } + if ($sourceLogin.DenyWindowsLogin) { + try { + $destLogin.DenyWindowsLogin = $true + } + catch { + $copyLoginStatus.Status = "Successful - but could not deny login on destination" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "$userName denied login on source, could not be denied login on $destination." -Category InvalidOperation -ErrorRecord $_ -Target $destServer 3>$null + } + } + } + if ($Pscmdlet.ShouldProcess($destination, "Updating SQL login $userName permissions")) { + Update-SqlPermissions -sourceserver $sourceServer -sourcelogin $sourceLogin -destserver $destServer -destlogin $destLogin + } + + if ($LoginRenameHashtable.Keys -contains $userName) { + $NewLogin = $LoginRenameHashtable[$userName] + + if ($Pscmdlet.ShouldProcess($destination, "Renaming SQL Login $userName to $NewLogin")) { + try { + Rename-DbaLogin -SqlInstance $destServer -Login $userName -NewLogin $NewLogin + + $copyLoginStatus.DestinationLogin = $NewLogin + $copyLoginStatus.Status = "Successful" + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + catch { + $copyLoginStatus.DestinationLogin = $NewLogin + $copyLoginStatus.Status = "Failed to rename" + $copyLoginStatus.Notes = $_.Exception.Message + $copyLoginStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue renaming $userName to $NewLogin" -Category InvalidOperation -ErrorRecord $_ -Target $destServer 3>$null + } + } + } + } #end for each $sourceLogin + } #end function Copy-Login + } + + process { + + if (Test-Bound -ParameterName InputObject) { + $Source = $InputObject[0].Parent.Name + $Sourceserver = $InputObject[0].Parent + $Login = $InputObject.Name + } + else { + Write-Message -Level Verbose -Message "Attempting to connect to SQL Servers." + $sourceServer = Connect-SqlInstance -RegularUser -SqlInstance $Source -SqlCredential $SourceSqlCredential + $source = $sourceServer.DomainInstanceName + } + + if ($Destination) { + $destServer = Connect-SqlInstance -RegularUser -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + $Destination = $destServer.DomainInstanceName + + $sourceVersionMajor = $sourceServer.VersionMajor + $destVersionMajor = $destServer.VersionMajor + if ($sourceVersionMajor -gt 10 -and $destVersionMajor -lt 11) { + Stop-Function -Message "Login migration from version $sourceVersionMajor to $destVersionMajor is not supported." -Category InvalidOperation -ErrorRecord $_ -Target $sourceServer + } + + if ($sourceVersionMajor -lt 8 -or $destVersionMajor -lt 8) { + Stop-Function -Message "SQL Server 7 and below are not supported." -Category InvalidOperation -ErrorRecord $_ -Target $sourceServer + } + } + + if ($SyncOnly) { + Sync-DbaSqlPermission -Source $sourceServer -Destination $destServer -Login $Login -ExcludeLogin $ExcludeLogin + return + } + + if ($OutFile) { + Export-DbaLogin -SqlInstance $sourceServer -FilePath $OutFile -Login $Login -ExcludeLogin $ExcludeLogin + return + } + + if ($Pscmdlet.ShouldProcess("console", "Showing migration attempt message")) { + Write-Message -Level Verbose -Message "Attempting Login Migration." + } + + Copy-Login -sourceserver $sourceServer -destserver $destServer -Login $Login -Exclude $ExcludeLogin + + if ($SyncSaName) { + $sa = $sourceServer.Logins | Where-Object id -eq 1 + $destSa = $destServer.Logins | Where-Object id -eq 1 + $saName = $sa.Name + if ($saName -ne $destSa.name) { + Write-Message -Level Verbose -Message "Changing sa username to match source ($saName)." + if ($Pscmdlet.ShouldProcess($destination, "Changing sa username to match source ($saName)")) { + $destSa.Rename($saName) + $destSa.Alter() + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlLogin + } +} +function Copy-DbaQueryStoreConfig { + <# + .SYNOPSIS + Copies the configuration of a Query Store enabled database and sets the copied configuration on other databases. + + .DESCRIPTION + Copies the configuration of a Query Store enabled database and sets the copied configuration on other databases. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2016 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER SourceDatabase + Specifies the database to copy the Query Store configuration from. + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2016 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationDatabase + Specifies a list of databases that will receive a copy of the Query Store configuration of the SourceDatabase. + + .PARAMETER Exclude + Specifies a list of databases which will NOT receive a copy of the Query Store configuration. + + .PARAMETER AllDatabases + If this switch is enabled, the Query Store configuration will be copied to all databases on the destination instance. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Enrico van de Laar ( @evdlaar ) + Tags: QueryStore + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-QueryStoreConfig + + .EXAMPLE + Copy-DbaQueryStoreConfig -Source ServerA\SQL -SourceDatabase AdventureWorks -Destination ServerB\SQL -AllDatabases + + Copy the Query Store configuration of the AdventureWorks database in the ServerA\SQL instance and apply it on all user databases in the ServerB\SQL Instance. + + .EXAMPLE + Copy-DbaQueryStoreConfig -Source ServerA\SQL -SourceDatabase AdventureWorks -Destination ServerB\SQL -DestinationDatabase WorldWideTraders + + Copy the Query Store configuration of the AdventureWorks database in the ServerA\SQL instance and apply it to the WorldWideTraders database in the ServerB\SQL Instance. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object]$SourceDatabase, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstanceParameter[]]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$DestinationDatabase, + [object[]]$Exclude, + [switch]$AllDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + + Write-Message -Message "Connecting to source: $Source." -Level Verbose + try { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + } + catch { + Stop-Function -Message "Can't connect to $Source." -InnerErrorRecord $_ -Target $Source + } + } + + PROCESS { + if (Test-FunctionInterrupt) { + return + } + # Grab the Query Store configuration from the SourceDatabase through the Get-DbaQueryStoreConfig function + $SourceQSConfig = Get-DbaQueryStoreConfig -SqlInstance $sourceServer -Database $SourceDatabase + + if (!$DestinationDatabase -and !$Exclude -and !$AllDatabases) { + Stop-Function -Message "You must specify databases to execute against using either -DestinationDatabase, -Exclude or -AllDatabases." -Continue + } + + foreach ($destinationServer in $Destination) { + + Write-Message -Message "Connecting to destination: $Destination." -Level Verbose + try { + $destServer = Connect-SqlInstance -SqlInstance $destinationServer -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Can't connect to $destinationServer." -InnerErrorRecord $_ -Target $destinationServer -Continue + } + + # We have to exclude all the system databases since they cannot have the Query Store feature enabled + $dbs = Get-DbaDatabase -SqlInstance $destServer -NoSystemDb + + if ($DestinationDatabase.count -gt 0) { + $dbs = $dbs | Where-Object { $DestinationDatabase -contains $_.Name } + } + + if ($Exclude.count -gt 0) { + $dbs = $dbs | Where-Object { $exclude -notcontains $_.Name } + } + + if ($dbs.count -eq 0) { + Stop-Function -Message "No matching databases found. Check the spelling and try again." -Continue + } + + foreach ($db in $dbs) { + # skipping the database if the source and destination are the same instance + if (($sourceServer.Name -eq $destinationServer) -and ($SourceDatabase -eq $db.Name)) { + continue + } + Write-Message -Message "Processing destination database: $db on $destinationServer." -Level Verbose + $copyQueryStoreStatus = [pscustomobject]@{ + SourceServer = $sourceServer.name + SourceDatabase = $SourceDatabase + DestinationServer = $destinationServer + Name = $db.name + Type = "QueryStore Configuration" + Status = $null + DateTime = [Sqlcollaborative.Dbatools.Utility.DbaDateTime](Get-Date) + } + + if ($db.IsAccessible -eq $false) { + $copyQueryStoreStatus.Status = "Skipped" + Stop-Function -Message "The database $db on server $destinationServer is not accessible. Skipping database." -Continue + } + + Write-Message -Message "Executing Set-DbaQueryStoreConfig." -Level Verbose + # Set the Query Store configuration through the Set-DbaQueryStoreConfig function + try { + $null = Set-DbaQueryStoreConfig -SqlInstance $destinationServer -SqlCredential $DestinationSqlCredential ` + -Database $db.name ` + -State $SourceQSConfig.ActualState ` + -FlushInterval $SourceQSConfig.FlushInterval ` + -CollectionInterval $SourceQSConfig.CollectionInterval ` + -MaxSize $SourceQSConfig.MaxSize ` + -CaptureMode $SourceQSConfig.CaptureMode ` + -CleanupMode $SourceQSConfig.CleanupMode ` + -StaleQueryThreshold $SourceQSConfig.StaleQueryThreshold + $copyQueryStoreStatus.Status = "Successful" + } + catch { + $copyQueryStoreStatus.Status = "Failed" + Stop-Function -Message "Issue setting Query Store on $db." -Target $db -InnerErrorRecord $_ -Continue + } + $copyQueryStoreStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + } +} +function Copy-DbaResourceGovernor { + <# + .SYNOPSIS + Migrates Resource Pools + + .DESCRIPTION + By default, all non-system resource pools are migrated. If the pool already exists on the destination, it will be skipped unless -Force is used. + + The -ResourcePool parameter is auto-populated for command-line completion and can be used to copy only specific objects. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2008 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ResourcePool + Specifies the resource pool(s) to process. Options for this list are auto-populated from the server. If unspecified, all resource pools will be processed. + + .PARAMETER ExcludeResourcePool + Specifies the resource pool(s) to exclude. Options for this list are auto-populated from the server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the policies will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, ResourceGovernor + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaResourceGovernor + + .EXAMPLE + Copy-DbaResourceGovernor -Source sqlserver2014a -Destination sqlcluster + + Copies all extended event policies from sqlserver2014a to sqlcluster using Windows credentials to connect to the SQL Server instances.. + + .EXAMPLE + Copy-DbaResourceGovernor -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all extended event policies from sqlserver2014a to sqlcluster using SQL credentials to connect to sqlserver2014a and Windows credentials to connect to sqlcluster. + + .EXAMPLE + Copy-DbaResourceGovernor -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$ResourcePool, + [object[]]$ExcludeResourcePool, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + process { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $sourceClassifierFunction = Get-DbaResourceGovernorClassifierFunction -SqlInstance $sourceServer + $destClassifierFunction = Get-DbaResourceGovernorClassifierFunction -SqlInstance $destServer + + $copyResourceGovSetting = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Resource Governor Settings" + Name = "All Settings" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + $copyResourceGovClassifierFunc = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Resource Governor Settings" + Name = "Classifier Function" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Pscmdlet.ShouldProcess($destination, "Updating Resource Governor settings")) { + if ($destServer.Edition -notmatch 'Enterprise' -and $destServer.Edition -notmatch 'Datacenter' -and $destServer.Edition -notmatch 'Developer') { + Write-Message -Level Verbose -Message "The resource governor is not available in this edition of SQL Server. You can manipulate resource governor metadata but you will not be able to apply resource governor configuration. Only Enterprise edition of SQL Server supports resource governor." + } + else { + try { + Write-Message -Level Verbose -Message "Managing classifier function." + if (!$sourceClassifierFunction) { + $copyResourceGovClassifierFunc.Status = "Skipped" + $copyResourceGovClassifierFunc.Notes = $null + $copyResourceGovClassifierFunc | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + else { + $fullyQualifiedFunctionName = $sourceClassifierFunction.Schema + "." + $sourceClassifierFunction.Name + + if (!$destClassifierFunction) { + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + $destFunction = $destServer.Databases["master"].UserDefinedFunctions[$sourceClassifierFunction.Name] + if ($destFunction) { + Write-Message -Level Verbose -Message "Dropping the function with the source classifier function name." + $destFunction.Drop() + } + + Write-Message -Level Verbose -Message "Creating function." + $destServer.Query($sourceClassifierFunction.Script()) + + $sql = "ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = $fullyQualifiedFunctionName);" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Mapping Resource Governor classifier function." + $destServer.Query($sql) + + $copyResourceGovClassifierFunc.Status = "Successful" + $copyResourceGovClassifierFunc.Notes = "The new classifier function has been created" + $copyResourceGovClassifierFunc | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + else { + if ($Force -eq $false) { + $copyResourceGovClassifierFunc.Status = "Skipped" + $copyResourceGovClassifierFunc.Notes = "A classifier function already exists" + $copyResourceGovClassifierFunc | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + else { + + $sql = "ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = NULL);" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Disabling the Resource Governor." + $destServer.Query($sql) + + $sql = "ALTER RESOURCE GOVERNOR RECONFIGURE;" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Reconfiguring Resource Governor." + $destServer.Query($sql) + + Write-Message -Level Verbose -Message "Dropping the destination classifier function." + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + $destFunction = $destServer.Databases["master"].UserDefinedFunctions[$sourceClassifierFunction.Name] + $destClassifierFunction.Drop() + + Write-Message -Level Verbose -Message "Re-creating the Resource Governor classifier function." + $destServer.Query($sourceClassifierFunction.Script()) + + $sql = "ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = $fullyQualifiedFunctionName);" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Mapping Resource Governor classifier function." + $destServer.Query($sql) + + $copyResourceGovClassifierFunc.Status = "Successful" + $copyResourceGovClassifierFunc.Notes = "The old classifier function has been overwritten." + $copyResourceGovClassifierFunc | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + } + } + catch { + $copyResourceGovSetting.Status = "Failed" + $copyResourceGovSetting.Notes = $_.Exception + $copyResourceGovSetting | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Not able to update settings." -Target $destServer -ErrorRecord $_ + } + } + } + + # Pools + if ($ResourcePool) { + $pools = $sourceServer.ResourceGovernor.ResourcePools | Where-Object Name -In $ResourcePool + } + elseif ($ExcludeResourcePool) { + $pool = $sourceServer.ResourceGovernor.ResourcePools | Where-Object Name -NotIn $ExcludeResourcePool + } + else { + $pools = $sourceServer.ResourceGovernor.ResourcePools | Where-Object { $_.Name -notin "internal", "default" } + } + + Write-Message -Level Verbose -Message "Migrating pools." + foreach ($pool in $pools) { + $poolName = $pool.Name + + $copyResourceGovPool = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Resource Governor Pool" + Name = $poolName + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destServer.ResourceGovernor.ResourcePools[$poolName]) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "Pool '$poolName' was skipped because it already exists on $destination. Use -Force to drop and recreate." + + $copyResourceGovPool.Status = "Skipped" + $copyResourceGovPool.Notes = "Already exists" + $copyResourceGovPool | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to drop $poolName")) { + Write-Message -Level Verbose -Message "Pool '$poolName' exists on $destination." + Write-Message -Level Verbose -Message "Force specified. Dropping $poolName." + + try { + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + $destPool = $destServer.ResourceGovernor.ResourcePools[$poolName] + $workloadGroups = $destPool.WorkloadGroups + foreach ($workloadGroup in $workloadGroups) { + $workloadGroup.Drop() + } + $destPool.Drop() + $destServer.ResourceGovernor.Alter() + } + catch { + $copyResourceGovPool.Status = "Failed to drop from Destination" + $copyResourceGovPool.Notes = $_.Exception + $copyResourceGovPool | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Unable to drop: $_ Moving on." -Target $destPool -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Migrating pool $poolName")) { + try { + $sql = $pool.Script() | Out-String + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Copying pool $poolName." + $destServer.Query($sql) + + $copyResourceGovPool.Status = "Successful" + $copyResourceGovPool | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + $workloadGroups = $pool.WorkloadGroups + foreach ($workloadGroup in $workloadGroups) { + $workgroupName = $workloadGroup.Name + + $copyResourceGovWorkGroup = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Resource Governor Pool Workgroup" + Name = $workgroupName + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + $sql = $workloadGroup.Script() | Out-String + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Copying $workgroupName." + $destServer.Query($sql) + + $copyResourceGovWorkGroup.Status = "Successful" + $copyResourceGovWorkGroup | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + catch { + $copyResourceGovWorkGroup.Status = "Failed" + $copyResourceGovWorkGroup.Notes = $_.Exception + $copyResourceGovWorkGroup | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Unable to migrate pool." -Target $pool -ErrorRecord $_ + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Reconfiguring")) { + if ($destServer.Edition -notmatch 'Enterprise' -and $destServer.Edition -notmatch 'Datacenter' -and $destServer.Edition -notmatch 'Developer') { + Write-Message -Level Verbose -Message "The resource governor is not available in this edition of SQL Server. You can manipulate resource governor metadata but you will not be able to apply resource governor configuration. Only Enterprise edition of SQL Server supports resource governor." + } + else { + + Write-Message -Level Verbose -Message "Reconfiguring Resource Governor." + try { + if (!$sourceServer.ResourceGovernor.Enabled) { + $sql = "ALTER RESOURCE GOVERNOR DISABLE" + $destServer.Query($sql) + } + else { + $sql = "ALTER RESOURCE GOVERNOR RECONFIGURE" + $destServer.Query($sql) + } + } catch { + $altermsg = $_.Exception + } + + + $copyResourceGovReconfig = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Reconfigure Resource Governor" + Name = "Reconfigure Resource Governor" + Status = "Successful" + Notes = $altermsg + DateTime = [DbaDateTime](Get-Date) + } + $copyResourceGovReconfig | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlResourceGovernor + } +} +function Copy-DbaServerAudit { + <# + .SYNOPSIS + Copy-DbaServerAudit migrates server audits from one SQL Server to another. + + .DESCRIPTION + By default, all audits are copied. The -Audit parameter is auto-populated for command-line completion and can be used to copy only specific audits. + + If the audit already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Audit + The audit(s) to process. Options for this list are auto-populated from the server. If unspecified, all audits will be processed. + + .PARAMETER ExcludeAudit + The audit(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the audits will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaServerAudit + + .EXAMPLE + Copy-DbaServerAudit -Source sqlserver2014a -Destination sqlcluster + + Copies all server audits from sqlserver2014a to sqlcluster, using Windows credentials. If audits with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaServerAudit -Source sqlserver2014a -Destination sqlcluster -Audit tg_noDbDrop -SourceSqlCredential $cred -Force + + Copies a single audit, the tg_noDbDrop audit from sqlserver2014a to sqlcluster, using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If an audit with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaServerAudit -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Audit, + [object[]]$ExcludeAudit, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 10 -or $destServer.VersionMajor -lt 10) { + Stop-Function -Message "Server Audits are only supported in SQL Server 2008 and above. Quitting." + return + } + + $serverAudits = $sourceServer.Audits + $destAudits = $destServer.Audits + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($currentAudit in $serverAudits) { + $auditName = $currentAudit.Name + + $copyAuditStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $auditName + Type = "Server Audit" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Audit -and $auditName -notin $Audit -or $auditName -in $ExcludeAudit) { + continue + } + + $sql = $currentAudit.Script() | Out-String + + if ($destAudits.Name -contains $auditName) { + if ($force -eq $false) { + $copyAuditStatus.Status = "Skipped" + $copyAuditStatus.Notes = "Already exists" + Write-Message -Level Verbose -Message "Server audit $auditName exists at destination. Use -Force to drop and migrate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping server audit $auditName")) { + try { + Write-Message -Level Verbose -Message "Dropping server audit $auditName." + foreach ($spec in $destServer.ServerAuditSpecifications) { + if ($auditSpecification.Auditname -eq $auditName) { + $auditSpecification.Drop() + } + } + + $destServer.audits[$auditName].Disable() + $destServer.audits[$auditName].Alter() + $destServer.audits[$auditName].Drop() + } + catch { + $copyAuditStatus.Status = "Failed" + $copyAuditStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping audit from destination." -Target $auditName -ErrorRecord $_ + } + } + } + } + + if ($null -ne ($currentAudit.Filepath) -AND (Test-DbaSqlPath -SqlInstance $destServer -Path $currentAudit.Filepath) -eq $false) { + if ($Force -eq $false) { + Write-Message -Level Verbose -Message "$($currentAudit.Filepath) does not exist on $destination. Skipping $auditName. Specify -Force to create the directory." + + $copyAuditStatus.Status = "Skipped" + $copyAuditStatus.Notes = "Already exists" + $copyAuditStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + Write-Message -Level Verbose -Message "Force specified. Creating directory." + + $destNetBios = Resolve-NetBiosName $destServer + $path = Join-AdminUnc $destNetBios $currentAudit.Filepath + $root = $currentAudit.Filepath.Substring(0, 3) + $rootUnc = Join-AdminUnc $destNetBios $root + + if ((Test-Path $rootUnc) -eq $true) { + try { + if ($Pscmdlet.ShouldProcess($destination, "Creating directory $($currentAudit.Filepath)")) { + $null = New-Item -ItemType Directory $currentAudit.Filepath -ErrorAction Continue + } + } + catch { + Write-Message -Level Verbose -Message "Couldn't create directory $($currentAudit.Filepath). Using default data directory." + $datadir = Get-SqlDefaultPaths $destServer data + $sql = $sql.Replace($currentAudit.FilePath, $datadir) + } + } + else { + $datadir = Get-SqlDefaultPaths $destServer data + $sql = $sql.Replace($currentAudit.FilePath, $datadir) + } + } + } + if ($Pscmdlet.ShouldProcess($destination, "Creating server audit $auditName")) { + try { + Write-Message -Level Verbose -Message "File path $($currentAudit.Filepath) exists on $Destination." + Write-Message -Level Verbose -Message "Copying server audit $auditName." + $destServer.Query($sql) + + $copyAuditStatus.Status = "Successful" + $copyAuditStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyAuditStatus.Status = "Failed" + $copyAuditStatus.Notes = $_.Exception + $copyAuditStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating audit." -Target $auditName -ErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlAudit + } +} +function Copy-DbaServerAuditSpecification { + <# + .SYNOPSIS + Copy-DbaServerAuditSpecification migrates server audit specifications from one SQL Server to another. + + .DESCRIPTION + By default, all audits are copied. The -AuditSpecification parameter is auto-populated for command-line completion and can be used to copy only specific audits. + + If the audit specification already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER AuditSpecification + The Server Audit Specification(s) to process. Options for this list are auto-populated from the server. If unspecified, all Server Audit Specifications will be processed. + + .PARAMETER ExcludeAuditSpecification + The Server Audit Specification(s) to exclude. Options for this list are auto-populated from the server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Audits Specifications will be dropped and recreated on Destination. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration,ServerAudit,AuditSpecification + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaServerAuditSpecification + + .EXAMPLE + Copy-DbaServerAuditSpecification -Source sqlserver2014a -Destination sqlcluster + + Copies all server audits from sqlserver2014a to sqlcluster using Windows credentials to connect. If audits with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaServerAuditSpecification -Source sqlserver2014a -Destination sqlcluster -ServerAuditSpecification tg_noDbDrop -SourceSqlCredential $cred -Force + + Copies a single audit, the tg_noDbDrop audit from sqlserver2014a to sqlcluster using SQL credentials to connect to sqlserver2014a and Windows credentials to connect to sqlcluster. If an audit specification with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaServerAuditSpecification -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [object[]]$AuditSpecification, + [object[]]$ExcludeAuditSpecification, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if (!(Test-SqlSa -SqlInstance $sourceServer -SqlCredential $SourceSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $source. Quitting." + return + } + + if (!(Test-SqlSa -SqlInstance $destServer -SqlCredential $DestinationSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $destination. Quitting." + return + } + + if ($sourceServer.VersionMajor -lt 10 -or $destServer.VersionMajor -lt 10) { + Stop-Function -Message "Server Audit Specifications are only supported in SQL Server 2008 and above. Quitting." + return + } + + if ($destServer.VersionMajor -lt $sourceServer.VersionMajor) { + Stop-Function -Message "Migration from version $($destServer.VersionMajor) to version $($sourceServer.VersionMajor) is not supported." + return + } + + $AuditSpecifications = $sourceServer.ServerAuditSpecifications + $destAudits = $destServer.ServerAuditSpecifications + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($auditSpec in $AuditSpecifications) { + $auditSpecName = $auditSpec.Name + + $copyAuditSpecStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Type = "Server Audit Specification" + Name = $auditSpecName + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($AuditSpecification -and $auditSpecName -notin $AuditSpecification -or $auditSpecName -in $ExcludeAuditSpecification) { + continue + } + + $destServer.Audits.Refresh() + + if ($destServer.Audits.Name -notcontains $auditSpec.AuditName) { + $copyAuditSpecStatus.Status = "Skipped" + $copyAuditSpecStatus.Notes = "Already exists" + Write-Message -Level Warning -Message "Audit $($auditSpec.AuditName) does not exist on $Destination. Skipping $auditSpecName." + continue + } + + if ($destAudits.name -contains $auditSpecName) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "Server audit $auditSpecName exists at destination. Use -Force to drop and migrate." + + $copyAuditSpecStatus.Status = "Skipped" + $copyAuditSpecStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping server audit $auditSpecName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping server audit $auditSpecName" + $destServer.ServerAuditSpecifications[$auditSpecName].Drop() + } + catch { + $copyAuditSpecStatus.Status = "Failed" + $copyAuditSpecStatus.Notes = $_.Exception + $copyAuditSpecStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping audit spec" -Target $auditSpecName -ErrorRecord $_ -Continue + } + } + } + } + if ($Pscmdlet.ShouldProcess($destination, "Creating server audit $auditSpecName")) { + try { + Write-Message -Level Verbose -Message "Copying server audit $auditSpecName" + $sql = $auditSpec.Script() | Out-String + Write-Message -Level Debug -Message $sql + $destServer.Query($sql) + + $copyAuditSpecStatus.Status = "Successful" + $copyAuditSpecStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyAuditSpecStatus.Status = "Failed" + $copyAuditSpecStatus.Notes = $_.Exception + $copyAuditSpecStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating audit spec on destination" -Target $auditSpecName -ErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlAuditSpecification + } +} +function Copy-DbaServerTrigger { + <# + .SYNOPSIS + Copy-DbaServerTrigger migrates server triggers from one SQL Server to another. + + .DESCRIPTION + By default, all triggers are copied. The -ServerTrigger parameter is auto-populated for command-line completion and can be used to copy only specific triggers. + + If the trigger already exists on the destination, it will be skipped unless -Force is used. + + .PARAMETER Source + Source SQL Server.You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ServerTrigger + The Server Trigger(s) to process - this list is auto-populated from the server. If unspecified, all Server Triggers will be processed. + + .PARAMETER ExcludeServerTrigger + The Server Trigger(s) to exclude - this list is auto-populated from the server + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + Drops and recreates the Trigger if it exists + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaServerTrigger + + .EXAMPLE + Copy-DbaServerTrigger -Source sqlserver2014a -Destination sqlcluster + + Copies all server triggers from sqlserver2014a to sqlcluster, using Windows credentials. If triggers with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaServerTrigger -Source sqlserver2014a -Destination sqlcluster -ServerTrigger tg_noDbDrop -SourceSqlCredential $cred -Force + + Copies a single trigger, the tg_noDbDrop trigger from sqlserver2014a to sqlcluster, using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. If a trigger with the same name exists on sqlcluster, it will be dropped and recreated because -Force was used. + + .EXAMPLE + Copy-DbaServerTrigger -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$ServerTrigger, + [object[]]$ExcludeServerTrigger, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if ($sourceServer.VersionMajor -lt 9 -or $destServer.VersionMajor -lt 9) { + Stop-Function -Message "Server Triggers are only supported in SQL Server 2005 and above. Quitting." + return + } + + if ($destServer.VersionMajor -lt $sourceServer.VersionMajor) { + Stop-Function -Message "Migration from version $($destServer.VersionMajor) to version $($sourceServer.VersionMajor) is not supported." + return + } + + $serverTriggers = $sourceServer.Triggers + $destTriggers = $destServer.Triggers + + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($trigger in $serverTriggers) { + $triggerName = $trigger.Name + + $copyTriggerStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $triggerName + Type = "Server Trigger" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($ServerTrigger -and $triggerName -notin $ServerTrigger -or $triggerName -in $ExcludeServerTrigger) { + continue + } + + if ($destTriggers.Name -contains $triggerName) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "Server trigger $triggerName exists at destination. Use -Force to drop and migrate." + + $copyTriggerStatus.Status = "Skipped" + $copyTriggerStatus.Status = "Already exists" + $copyTriggerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Dropping server trigger $triggerName and recreating")) { + try { + Write-Message -Level Verbose -Message "Dropping server trigger $triggerName" + $destServer.Triggers[$triggerName].Drop() + } + catch { + $copyTriggerStatus.Status = "Failed" + $copyTriggerStatus.Notes = $_.Exception + $copyTriggerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping trigger on destination" -Target $triggerName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating server trigger $triggerName")) { + try { + Write-Message -Level Verbose -Message "Copying server trigger $triggerName" + $sql = $trigger.Script() | Out-String + $sql = $sql -replace "CREATE TRIGGER", "`nGO`nCREATE TRIGGER" + $sql = $sql -replace "ENABLE TRIGGER", "`nGO`nENABLE TRIGGER" + Write-Message -Level Debug -Message $sql + + foreach ($query in ($sql -split '\nGO\b')) { + $destServer.Query($query) | Out-Null + } + + $copyTriggerStatus.Status = "Successful" + $copyTriggerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyTriggerStatus.Status = "Failed" + $copyTriggerStatus.Notes = $_.Exception + $copyTriggerStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating trigger on destination" -Target $triggerName -ErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlServerTrigger + } +} +function Copy-DbaSpConfigure { + <# + .SYNOPSIS + Copy-DbaSpConfigure migrates configuration values from one SQL Server to another. + + .DESCRIPTION + By default, all configuration values are copied. The -ConfigName parameter is auto-populated for command-line completion and can be used to copy only specific configs. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ConfigName + Specifies the configuration setting to process. Options for this list are auto-populated from the server. If unspecified, all ConfigNames will be processed. + + .PARAMETER ExcludeConfigName + Specifies the configuration settings to exclude. Options for this list are auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Configure, SpConfigure + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSpConfigure + + .EXAMPLE + Copy-DbaSpConfigure -Source sqlserver2014a -Destination sqlcluster + + Copies all sp_configure settings from sqlserver2014a to sqlcluster + + .EXAMPLE + Copy-DbaSpConfigure -Source sqlserver2014a -Destination sqlcluster -ConfigName DefaultBackupCompression, IsSqlClrEnabled -SourceSqlCredential $cred -Force + + Copies the values for IsSqlClrEnabled and DefaultBackupCompression from sqlserver2014a to sqlcluster using SQL credentials to authenticate to sqlserver2014a and Windows credentials to authenticate to sqlcluster. + + .EXAMPLE + Copy-DbaSpConfigure -Source sqlserver2014a -Destination sqlcluster -ExcludeConfigName DefaultBackupCompression, IsSqlClrEnabled + + Copies all configs except for IsSqlClrEnabled and DefaultBackupCompression, from sqlserver2014a to sqlcluster. + + .EXAMPLE + Copy-DbaSpConfigure -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$ConfigName, + [object[]]$ExcludeConfigName, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + } + process { + + $sourceProps = Get-DbaSpConfigure -SqlInstance $sourceServer + $destProps = Get-DbaSpConfigure -SqlInstance $destServer + + foreach ($sourceProp in $sourceProps) { + $displayName = $sourceProp.DisplayName + $sConfigName = $sourceProp.ConfigName + $sConfiguredValue = $sourceProp.ConfiguredValue + $requiresRestart = $sourceProp.IsDynamic + + $copySpConfigStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $sConfigName + Type = "Configuration Value" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($ConfigName -and $sConfigName -notin $ConfigName -or $sConfigName -in $ExcludeConfigName ) { + continue + } + + $destProp = $destProps | Where-Object ConfigName -eq $sConfigName + if (!$destProp) { + Write-Message -Level Verbose -Message "Configuration $sConfigName ('$displayName') does not exist on the destination instance." + + $copySpConfigStatus.Status = "Skipped" + $copySpConfigStatus.Notes = "Configuration does not exist on destination" + $copySpConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + continue + } + + if ($Pscmdlet.ShouldProcess($destination, "Updating $sConfigName [$displayName]")) { + try { + $destOldConfigValue = $destProp.ConfiguredValue + + if ($sConfiguredValue -ne $destOldConfigValue) { + $result = Set-DbaSpConfigure -SqlInstance $destServer -Name $sConfigName -Value $sConfiguredValue -EnableException -WarningAction SilentlyContinue + if ($result) { + Write-Message -Level Verbose -Message "Updated $($destProp.ConfigName) ($($destProp.DisplayName)) from $destOldConfigValue to $sConfiguredValue." + } + } + if ($requiresRestart -eq $false) { + Write-Message -Level Verbose -Message "Configuration option $sConfigName ($displayName) requires restart." + $copySpConfigStatus.Notes = "Requires restart" + } + $copySpConfigStatus.Status = "Successful" + $copySpConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + if ($_.Exception -match 'the same as the') { + $copySpConfigStatus.Status = "Successful" + } + else { + $copySpConfigStatus.Status = "Failed" + $copySpConfigStatus.Notes = $_.Exception + } + $copySpConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Could not set $($destProp.ConfigName) to $sConfiguredValue." -Target $sConfigName -ErrorRecord $_ + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlSpConfigure + } +} +function Copy-DbaSqlDataCollector { + <# + .SYNOPSIS + Migrates user SQL Data Collector collection sets. SQL Data Collector configuration is on the agenda, but it's hard. + + .DESCRIPTION + By default, all data collector objects are migrated. If the object already exists on the destination, it will be skipped unless -Force is used. + + The -CollectionSet parameter is auto-populated for command-line completion and can be used to copy only specific objects. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER CollectionSet + The collection set(s) to process - this list is auto-populated from the server. If unspecified, all collection sets will be processed. + + .PARAMETER ExcludeCollectionSet + The collection set(s) to exclude - this list is auto-populated from the server + + .PARAMETER NoServerReconfig + Upcoming parameter to enable server reconfiguration + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + If collection sets exists on destination server, it will be dropped and recreated. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration,DataCollection + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSqlDataCollector + + .EXAMPLE + Copy-DbaSqlDataCollector -Source sqlserver2014a -Destination sqlcluster + + Copies all Data Collector Objects and Configurations from sqlserver2014a to sqlcluster, using Windows credentials. + + .EXAMPLE + Copy-DbaSqlDataCollector -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all Data Collector Objects and Configurations from sqlserver2014a to sqlcluster, using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. + + .EXAMPLE + Copy-DbaSqlDataCollector -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + + .EXAMPLE + Copy-DbaSqlDataCollector -Source sqlserver2014a -Destination sqlcluster -CollectionSet 'Server Activity', 'Table Usage Analysis' + + Copies two Collection Sets, Server Activity and Table Usage Analysis, from sqlserver2014a to sqlcluster. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$CollectionSet, + [object[]]$ExcludeCollectionSet, + [switch]$NoServerReconfig, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential -MinimumVersion 10 + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential -MinimumVersion 10 + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + } + process { + if (Test-FunctionInterrupt) { return } + + if ($NoServerReconfig -eq $false) { + Write-Message -Level Verbose -Message "Server reconfiguration not yet supported. Only Collection Set migration will be migrated at this time." + $NoServerReconfig = $true + + <# for future use when this support is added #> + $copyServerConfigStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $userName + Type = "Data Collection Server Config" + Status = "Skipped" + Notes = "Not supported at this time" + DateTime = [DbaDateTime](Get-Date) + } + $copyServerConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + } + + $sourceSqlConn = $sourceServer.ConnectionContext.SqlConnectionObject + $sourceSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $sourceSqlConn + $sourceStore = New-Object Microsoft.SqlServer.Management.Collector.CollectorConfigStore $sourceSqlStoreConnection + + $destSqlConn = $destServer.ConnectionContext.SqlConnectionObject + $destSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $destSqlConn + $destStore = New-Object Microsoft.SqlServer.Management.Collector.CollectorConfigStore $destSqlStoreConnection + + $configDb = $sourceStore.ScriptAlter().GetScript() | Out-String + + $configDb = $configDb -replace [Regex]::Escape("'$source'"), "'$destReplace'" + + if (!$NoServerReconfig) { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to modify Data Collector configuration")) { + try { + $sql = "Unknown at this time" + $destServer.Query($sql) + $destStore.Alter() + } + catch { + $copyServerConfigStatus.Status = "Failed" + $copyServerConfigStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue modifying Data Collector configuration" -Target $destServer -ErrorRecord $_ + } + } + } + + if ($destStore.Enabled -eq $false) { + Write-Message -Level Verbose -Message "The Data Collector must be setup initially for Collection Sets to be migrated. Setup the Data Collector and try again." + return + } + + $storeCollectionSets = $sourceStore.CollectionSets | Where-Object { $_.IsSystem -eq $false } + if ($CollectionSet) { + $storeCollectionSets = $storeCollectionSets | Where-Object Name -In $CollectionSet + } + if ($ExcludeCollectionSet) { + $storeCollectionSets = $storeCollectionSets | Where-Object Name -NotIn $ExcludeCollectionSet + } + + Write-Message -Level Verbose -Message "Migrating collection sets" + foreach ($set in $storeCollectionSets) { + $collectionName = $set.Name + + $copyCollectionSetStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $collectionName + Type = "Collection Set" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destStore.CollectionSets[$collectionName]) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "Collection Set '$collectionName' was skipped because it already exists on $destination. Use -Force to drop and recreate" + + $copyCollectionSetStatus.Status = "Skipped" + $copyCollectionSetStatus.Notes = "Already exists" + $copyCollectionSetStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to drop $collectionName")) { + Write-Message -Level Verbose -Message "Collection Set '$collectionName' exists on $destination" + Write-Message -Level Verbose -Message "Force specified. Dropping $collectionName." + + try { + $destStore.CollectionSets[$collectionName].Drop() + } + catch { + $copyCollectionSetStatus.Status = "Failed to drop on destination" + $copyCollectionSetStatus.Notes = $_.Exception + $copyCollectionSetStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping collection" -Target $collectionName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Migrating collection set $collectionName")) { + try { + $sql = $set.ScriptCreate().GetScript() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Migrating collection set $collectionName" + $destServer.Query($sql) + + $copyCollectionSetStatus.Status = "Successful" + $copyCollectionSetStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyCollectionSetStatus.Status = "Failed to create collection" + $copyCollectionSetStatus.Notes = $_.Exception + + Stop-Function -Message "Issue creating collection set" -Target $collectionName -ErrorRecord $_ + } + + try { + if ($set.IsRunning) { + Write-Message -Level Verbose -Message "Starting collection set $collectionName" + $destStore.CollectionSets.Refresh() + $destStore.CollectionSets[$collectionName].Start() + } + + $copyCollectionSetStatus.Status = "Successful started Collection" + $copyCollectionSetStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyCollectionSetStatus.Status = "Failed to start collection" + $copyCollectionSetStatus.Notes = $_.Exception + $copyCollectionSetStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue starting collection set" -Target $collectionName -ErrorRecord $_ + } + } + } + } + end { + if (Test-FunctionInterrupt) { return } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlDataCollector + } +} +function Copy-DbaSqlPolicyManagement { + <# + .SYNOPSIS + Migrates SQL Policy Based Management Objects, including both policies and conditions. + + .DESCRIPTION + By default, all policies and conditions are copied. If an object already exist on the destination, it will be skipped unless -Force is used. + + The -Policy and -Condition parameters are auto-populated for command-line completion and can be used to copy only specific objects. + + .PARAMETER Source + Source SQL Server.You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Policy + The policy(ies) to process - this list is auto-populated from the server. If unspecified, all policies will be processed. + + .PARAMETER ExcludePolicy + The policy(ies) to exclude - this list is auto-populated from the server + + .PARAMETER Condition + The condition(s) to process - this list is auto-populated from the server. If unspecified, all conditions will be processed. + + .PARAMETER ExcludeCondition + The condition(s) to exclude - this list is auto-populated from the server + + .PARAMETER Force + If policies exists on destination server, it will be dropped and recreated. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSqlPolicyManagement + + .EXAMPLE + Copy-DbaSqlPolicyManagement -Source sqlserver2014a -Destination sqlcluster + + Copies all policies and conditions from sqlserver2014a to sqlcluster, using Windows credentials. + + .EXAMPLE + Copy-DbaSqlPolicyManagement -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all policies and conditions from sqlserver2014a to sqlcluster, using SQL credentials for sqlserver2014a and Windows credentials for sqlcluster. + + .EXAMPLE + Copy-DbaSqlPolicyManagement -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + + .EXAMPLE + Copy-DbaSqlPolicyManagement -Source sqlserver2014a -Destination sqlcluster -Policy 'xp_cmdshell must be disabled' + + Copies only one policy, 'xp_cmdshell must be disabled' from sqlserver2014a to sqlcluster. No conditions are migrated. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential] + $SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential] + $DestinationSqlCredential, + [object[]]$Policy, + [object[]]$ExcludePolicy, + [object[]]$Condition, + [object[]]$ExcludeCondition, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential -MinimumVersion 10 + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential -MinimumVersion 10 + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + } + process { + if (Test-FunctionInterrupt) { return } + + $sourceSqlConn = $sourceServer.ConnectionContext.SqlConnectionObject + $sourceSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $sourceSqlConn + $sourceStore = New-Object Microsoft.SqlServer.Management.DMF.PolicyStore $sourceSqlStoreConnection + + $destSqlConn = $destServer.ConnectionContext.SqlConnectionObject + $destSqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $destSqlConn + $destStore = New-Object Microsoft.SqlServer.Management.DMF.PolicyStore $destSqlStoreConnection + + $storePolicies = $sourceStore.Policies | Where-Object { $_.IsSystemObject -eq $false } + $storeConditions = $sourceStore.Conditions | Where-Object { $_.IsSystemObject -eq $false } + + if ($Policy) { + $storePolicies = $storePolicies | Where-Object Name -In $Policy + } + if ($ExcludePolicy) { + $storePolicies = $storePolicies | Where-Object Name -NotIn $ExcludePolicy + } + if ($Condition) { + $storeConditions = $storeConditions | Where-Object Name -In $Condition + } + if ($ExcludeCondition) { + $storeConditions = $storeConditions | Where-Object Name -NotIn $ExcludeCondition + } + + if ($Policy -and $Condition) { + $storeConditions = $null + $storePolicies = $null + } + + <# + Conditions + #> + + Write-Message -Level Verbose -Message "Migrating conditions" + foreach ($condition in $storeConditions) { + $conditionName = $condition.Name + + $copyConditionStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $conditionName + Type = "Policy Condition" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destStore.Conditions[$conditionName]) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "condition '$conditionName' was skipped because it already exists on $destination. Use -Force to drop and recreate" + + $copyConditionStatus.Status = "Skipped" + $copyConditionStatus.Notes = "Already exists" + $copyConditionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to drop $conditionName")) { + Write-Message -Level Verbose -Message "Condition '$conditionName' exists on $destination. Force specified. Dropping $conditionName." + + try { + $dependentPolicies = $destStore.Conditions[$conditionName].EnumDependentPolicies() + foreach ($dependent in $dependentPolicies) { + $dependent.Drop() + $destStore.Conditions.Refresh() + } + $destStore.Conditions[$conditionName].Drop() + } + catch { + $copyConditionStatus.Status = "Failed" + $copyConditionStatus.Notes = $_.Exception.Message + $copyConditionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message "Issue dropping condition on $destination" -Target $conditionName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Migrating condition $conditionName")) { + try { + $sql = $condition.ScriptCreate().GetScript() | Out-String + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Copying condition $conditionName" + $null = $destServer.Query($sql) + $destStore.Conditions.Refresh() + + $copyConditionStatus.Status = "Successful" + $copyConditionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyConditionStatus.Status = "Failed" + $copyConditionStatus.Notes = $_.Exception.Message + $copyConditionStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue creating condition on $destination" -Target $conditionName -ErrorRecord $_ + } + } + } + + <# + Policies + #> + + Write-Message -Level Verbose -Message "Migrating policies" + foreach ($policy in $storePolicies) { + $policyName = $policy.Name + + $copyPolicyStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = $policyName + Type = "Policy" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($null -ne $destStore.Policies[$policyName]) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "Policy '$policyName' was skipped because it already exists on $destination. Use -Force to drop and recreate" + + $copyPolicyStatus.Status = "Skipped" + $copyPolicyStatus.Notes = "Already exists" + $copyPolicyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + continue + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Attempting to drop $policyName")) { + Write-Message -Level Verbose -Message "Policy '$policyName' exists on $destination. Force specified. Dropping $policyName." + + try { + $destStore.Policies[$policyName].Drop() + $destStore.Policies.refresh() + } + catch { + $copyPolicyStatus.Status = "Failed" + $copyPolicyStatus.Notes = $_.Exception.Message + $copyPolicyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + Stop-Function -Message "Issue dropping policy on $destination" -Target $policyName -ErrorRecord $_ -Continue + } + } + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Migrating policy $policyName")) { + try { + $destStore.Conditions.Refresh() + $destStore.Policies.Refresh() + $sql = $policy.ScriptCreateWithDependencies().GetScript() | Out-String + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Copying policy $policyName" + $null = $destServer.Query($sql) + + $copyPolicyStatus.Status = "Successful" + $copyPolicyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $copyPolicyStatus.Status = "Failed" + $copyPolicyStatus.Notes = $_.Exception.Message + $copyPolicyStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + + # This is usually because of a duplicate dependent from above. Just skip for now. + Stop-Function -Message "Issue creating policy on $destination" -Target $policyName -ErrorRecord $_ -Continue + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlPolicyManagement + } +} +function Copy-DbaSqlServerAgent { + <# + .SYNOPSIS + Copy SQL Server Agent from one server to another. + + .DESCRIPTION + A wrapper function that calls the associated Copy command for each of the object types seen in SSMS under SQL Server Agent. This also copies all of the the SQL Agent properties (job history max rows, DBMail profile name, etc.). + + You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DisableJobsOnDestination + If this switch is enabled, the jobs will be disabled on Destination after copying. + + .PARAMETER DisableJobsOnSource + If this switch is enabled, the jobs will be disabled on Source after copying. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, existing objects on Destination with matching names from Source will be dropped, then copied. + + .NOTES + Tags: Migration, SqlServerAgent, SqlAgent + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSqlServerAgent + + .EXAMPLE + Copy-DbaSqlServerAgent -Source sqlserver2014a -Destination sqlcluster + + Copies all job server objects from sqlserver2014a to sqlcluster using Windows credentials for authentication. If job objects with the same name exist on sqlcluster, they will be skipped. + + .EXAMPLE + Copy-DbaSqlServerAgent -Source sqlserver2014a -Destination sqlcluster -SourceSqlCredential $cred + + Copies all job objects from sqlserver2014a to sqlcluster using SQL credentials to authentication to sqlserver2014a and Windows credentials to authenticate to sqlcluster. + + .EXAMPLE + Copy-DbaSqlServerAgent -Source sqlserver2014a -Destination sqlcluster -WhatIf + + Shows what would happen if the command were executed. + #> + [cmdletbinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [Switch]$DisableJobsOnDestination, + [Switch]$DisableJobsOnSource, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + Invoke-SmoCheck -SqlInstance $sourceServer + Invoke-SmoCheck -SqlInstance $destServer + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + $sourceAgent = $sourceServer.JobServer + } + process { + + # All of these support whatif inside of them + Copy-DbaAgentCategory -Source $sourceServer -Destination $destServer -Force:$force + + $destServer.JobServer.JobCategories.Refresh() + $destServer.JobServer.OperatorCategories.Refresh() + $destServer.JobServer.AlertCategories.Refresh() + + Copy-DbaAgentOperator -Source $sourceServer -Destination $destServer -Force:$force + $destServer.JobServer.Operators.Refresh() + + Copy-DbaAgentAlert -Source $sourceServer -Destination $destServer -Force:$force -IncludeDefaults + $destServer.JobServer.Alerts.Refresh() + + Copy-DbaAgentProxyAccount -Source $sourceServer -Destination $destServer -Force:$force + $destServer.JobServer.ProxyAccounts.Refresh() + + Copy-DbaAgentSharedSchedule -Source $sourceServer -Destination $destServer -Force:$force + $destServer.JobServer.SharedSchedules.Refresh() + + $destServer.JobServer.Refresh() + $destServer.Refresh() + Copy-DbaAgentJob -Source $sourceServer -Destination $destServer -Force:$force -DisableOnDestination:$DisableJobsOnDestination -DisableOnSource:$DisableJobsOnSource + + # To do + <# + Copy-DbaAgentMasterServer -Source $sourceServer -Destination $destServer -Force:$force + Copy-DbaAgentTargetServer -Source $sourceServer -Destination $destServer -Force:$force + Copy-DbaAgentTargetServerGroup -Source $sourceServer -Destination $destServer -Force:$force + #> + + <# Here are the properties which must be migrated separately #> + $copyAgentPropStatus = [pscustomobject]@{ + SourceServer = $sourceServer.Name + DestinationServer = $destServer.Name + Name = "Server level properties" + Type = "Agent Properties" + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + + if ($Pscmdlet.ShouldProcess($destination, "Copying Agent Properties")) { + try { + Write-Message -Level Verbose -Message "Copying SQL Agent Properties" + $sql = $sourceAgent.Script() | Out-String + $sql = $sql -replace [Regex]::Escape("'$source'"), "'$destination'" + $sql = $sql -replace [Regex]::Escape("@errorlog_file="), [Regex]::Escape("--@errorlog_file=") + $sql = $sql -replace [Regex]::Escape("@auto_start="), [Regex]::Escape("--@auto_start=") + Write-Message -Level Debug -Message $sql + $null = $destServer.Query($sql) + + $copyAgentPropStatus.Status = "Successful" + $copyAgentPropStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + } + catch { + $message = $_.Exception.InnerException.InnerException.InnerException.Message + if (-not $message) { $message = $_.Exception.Message } + $copyAgentPropStatus.Status = "Failed" + $copyAgentPropStatus.Notes = $message + $copyAgentPropStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject + Stop-Function -Message $message -Target $destination + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlServerAgent + } +} +#ValidationTags#Messaging# +function Copy-DbaSsisCatalog { + <# + .SYNOPSIS + Copy-DbaSsisCatalog migrates Folders, SSIS projects, and environments from one SQL Server to another. + + .DESCRIPTION + By default, all folders, projects, and environments are copied. The -Project parameter can be specified to copy only one project, if desired. + + The parameters get more granular from the Folder level. For example, specifying -Folder will only deploy projects/environments from within that folder. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2012 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2012 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Force + If this switch is enabled, the SSIS Catalog will be dropped and recreated on Destination if it already exists. + + .PARAMETER Project + Specifies a source Project name. + + .PARAMETER Folder + Specifies a source folder name. + + .PARAMETER Environment + Specifies an environment to copy. + + .PARAMETER EnableSqlClr + If this switch is enabled and Destination does not have the SQL CLR configuration option enabled, user prompts for enabling it on Destination will be skipped. SQL CLR is required for SSISDB. + + .PARAMETER CreateCatalogPassword + Specifies a secure string to use in creating an SSISDB catalog on Destination. If this is specified, prompts for the password will be skipped. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, SSIS + Author: Phil Schwartz (philschwartz.me, @pschwartzzz) + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSsisCatalog + + .EXAMPLE + Copy-DbaSsisCatalog -Source sqlserver2014a -Destination sqlcluster + + Copies all folders, environments and SSIS Projects from sqlserver2014a to sqlcluster, using Windows credentials to authenticate to both instances. If folders with the same name exist on the destination they will be skipped, but projects will be redeployed. + + .EXAMPLE + Copy-DbaSsisCatalog -Source sqlserver2014a -Destination sqlcluster -Project Archive_Tables -SourceSqlCredential $cred -Force + + Copies a single Project, the Archive_Tables Project, from sqlserver2014a to sqlcluster using SQL credentials to authenticate to sqlserver2014a and Windows credentials to authenticate to sqlcluster. If a Project with the same name exists on sqlcluster, it will be deleted and recreated because -Force was used. + + .EXAMPLE + Copy-DbaSsisCatalog -Source sqlserver2014a -Destination sqlcluster -WhatIf -Force + + Shows what would happen if the command were executed using force. + + .EXAMPLE + $SecurePW = Read-Host "Enter password" -AsSecureString + Copy-DbaSsisCatalog -Source sqlserver2014a -Destination sqlcluster -CreateCatalogPassword $SecurePW + + Deploy entire SSIS catalog to an instance without a destination catalog. User prompts for creating the catalog on Destination will be bypassed. + + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Source, + [parameter(Mandatory = $true)] + [DbaInstanceParameter]$Destination, + [PSCredential]$SourceSqlCredential, + [PSCredential]$DestinationSqlCredential, + [String]$Project, + [String]$Folder, + [String]$Environment, + [System.Security.SecureString]$CreateCatalogPassword, + [Switch]$EnableSqlClr, + [Switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + <# Developer note: The throw calls must stay in this command #> + begin { + function Get-RemoteIntegrationService { + param ( + [Object]$Computer + ) + $result = Get-DbaSqlService -ComputerName $Computer -Type SSIS + if ($result) { + $running = $false + foreach ($service in $result) { + if (!$service.State -eq "Running") { + Write-Message -Level Warning -Message "Service $($service.DisplayName) was found on the destination, but is currently not running." + } + else { + Write-Message -Level Verbose -Message "Service $($service.DisplayName) was found running on the destination." + $running = $true + } + } + } + else { + throw "No Integration Services service was found on the destination, please ensure the feature is installed and running." + } + } + + function Invoke-ProjectDeployment { + param ( + [String]$Project, + [String]$Folder + ) + $sqlConn = New-Object System.Data.SqlClient.SqlConnection + $sqlConn.ConnectionString = $sourceConnection.ConnectionContext.ConnectionString + if ($sqlConn.State -eq "Closed") { + $sqlConn.Open() + } + try { + Write-Message -Level Verbose -Message "Deploying project $Project from folder $Folder." + $cmd = New-Object System.Data.SqlClient.SqlCommand + $cmd.CommandType = "StoredProcedure" + $cmd.connection = $sqlConn + $cmd.CommandText = "SSISDB.Catalog.get_project" + $cmd.Parameters.Add("@folder_name", $Folder) | out-null; + $cmd.Parameters.Add("@project_name", $Project) | out-null; + [byte[]]$results = $cmd.ExecuteScalar(); + if ($null -ne $results) { + $destFolder = $destinationFolders | Where-Object { $_.Name -eq $Folder } + $deployedProject = $destFolder.DeployProject($Project, $results) + if ($deployedProject.Status -ne "Success") { + Stop-Function -Message "An error occurred deploying project $Project." -Target $Project -Continue + } + } + else { + Stop-Function -Message "Failed deploying $Project from folder $Folder." -Target $Project -Continue + } + } + catch { + Stop-Function -Message "Failed to deploy project." -Target $Project -ErrorRecord $_ + } + finally { + if ($sqlConn.State -eq "Open") { + $sqlConn.Close() + } + } + } + function New-CatalogFolder { + param ( + [String]$Folder, + [String]$Description, + [Switch]$Force + ) + if ($Force) { + $remove = $destinationFolders | Where-Object { $_.Name -eq $Folder } + $envs = $remove.Environments.Name + foreach ($e in $envs) { + $remove.Environments[$e].Drop() + } + $projs = $remove.Projects.Name + foreach ($p in $projs) { + $remove.Projects[$p].Drop() + } + $remove.Drop() + $destinationCatalog.Alter() + $destinationCatalog.Refresh() + } + Write-Message -Level Verbose -Message "Creating folder $Folder." + $destFolder = New-Object "$ISNamespace.CatalogFolder" ($destinationCatalog, $Folder, $Description) + $destFolder.Create() + $destFolder.Alter() + $destFolder.Refresh() + } + function New-FolderEnvironment { + param ( + [String]$Folder, + [String]$Environment, + [Switch]$Force + ) + $envDestFolder = $destinationFolders | Where-Object { $_.Name -eq $Folder } + if ($force) { + $envDestFolder.Environments[$Environment].Drop() + $envDestFolder.Alter() + $envDestFolder.Refresh() + } + $srcEnv = ($sourceFolders | Where-Object { $_.Name -eq $Folder }).Environments[$Environment] + $targetEnv = New-Object "$ISNamespace.EnvironmentInfo" ($envDestFolder, $srcEnv.Name, $srcEnv.Description) + foreach ($var in $srcEnv.Variables) { + if ($var.Value.ToString() -eq "") { + $finalValue = "" + } + else { + $finalValue = $var.Value + } + $targetEnv.Variables.Add($var.Name, $var.Type, $finalValue, $var.Sensitive, $var.Description) + } + Write-Message -Level Verbose -Message "Creating environment $Environment." + $targetEnv.Create() + $targetEnv.Alter() + $targetEnv.Refresh() + } + function New-SSISDBCatalog { + param ( + [System.Security.SecureString]$Password + ) + + if (!$Password) { + Write-Message -Level Verbose -Message "SSISDB Catalog requires a password." + $pass1 = Read-Host "Enter a password" -AsSecureString + $plainTextPass1 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass1)) + $pass2 = Read-Host "Re-enter password" -AsSecureString + $plainTextPass2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass2)) + if ($plainTextPass1 -ne $plainTextPass2) { + throw "Validation error, passwords entered do not match." + } + $plainTextPass = $plainTextPass1 + } + else { + $plainTextPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)) + } + + $catalog = New-Object "$ISNamespace.Catalog" ($destinationSSIS, "SSISDB", $plainTextPass) + $catalog.Create() + $catalog.Refresh() + } + + $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices" + + $sourceConnection = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destinationConnection = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + if ($sourceConnection.versionMajor -lt 11 -or $destinationConnection.versionMajor -lt 11) { + Stop-Function -Message "SSISDB catalog is only available on Sql Server 2012 and above, exiting..." + } + + try { + Get-RemoteIntegrationService -Computer $Destination + } + catch { + Stop-Function -Message "An error occurred when checking the destination for Integration Services. Is Integration Services installed?" -Target $Destination -ErrorRecord $_ + } + + try { + Write-Message -Level Verbose -Message "Connecting to $Source integration services." + $sourceSSIS = New-Object "$ISNamespace.IntegrationServices" $sourceConnection + } + catch { + Stop-Function -Message "There was an error connecting to the source integration services." -Target $sourceConnection -ErrorRecord $_ + } + try { + Write-Message -Level Verbose -Message "Connecting to $Destination integration services." + $destinationSSIS = New-Object "$ISNamespace.IntegrationServices" $destinationConnection + } + catch { + Stop-Function -Message "There was an error connecting to the destination integration services." -Target $destinationCon -ErrorRecord $_ + } + + $sourceCatalog = $sourceSSIS.Catalogs | Where-Object { $_.Name -eq "SSISDB" } + $destinationCatalog = $destinationSSIS.Catalogs | Where-Object { $_.Name -eq "SSISDB" } + + $sourceFolders = $sourceCatalog.Folders + $destinationFolders = $destinationCatalog.Folders + } + process { + + if (!$sourceCatalog) { + throw "The source SSISDB catalog does not exist." + } + if (!$destinationCatalog) { + if (!$destinationConnection.Configuration.IsSqlClrEnabled.ConfigValue) { + if ($Pscmdlet.ShouldProcess($Destination, "Enabling SQL CLR configuration option.")) { + if (!$EnableSqlClr) { + $message = "The destination does not have SQL CLR configuration option enabled (required by SSISDB), would you like to enable it?" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Enable SQL CLR on $Destination." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($null, $message, $options, 0) + switch ($result) { + 0 { + continue + } + 1 { + return + } + } + } + Write-Message -Level Verbose -Message "Enabling SQL CLR configuration option at the destination." + if ($destinationConnection.Configuration.ShowAdvancedOptions.ConfigValue -eq $false) { + $destinationConnection.Configuration.ShowAdvancedOptions.ConfigValue = $true + $changeback = $true + } + + $destinationConnection.Configuration.IsSqlClrEnabled.ConfigValue = $true + + if ($changeback -eq $true) { + $destinationConnection.Configuration.ShowAdvancedOptions.ConfigValue = $false + } + $destinationConnection.Configuration.Alter() + } + } + else { + Write-Message -Level Verbose -Message "SQL CLR configuration option is already enabled at the destination." + } + if ($Pscmdlet.ShouldProcess($Destination, "Create destination SSISDB Catalog")) { + if (!$CreateCatalogPassword) { + $message = "The destination SSISDB catalog does not exist, would you like to create one?" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Create an SSISDB catalog on $Destination." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($null, $message, $options, 0) + switch ($result) { + 0 { + New-SSISDBCatalog + } + 1 { + return + } + } + } + else { + New-SSISDBCatalog -Password $CreateCatalogPassword + } + + $destinationSSIS.Refresh() + $destinationCatalog = $destinationSSIS.Catalogs | Where-Object { $_.Name -eq "SSISDB" } + $destinationFolders = $destinationCatalog.Folders + } + else { + throw "The destination SSISDB catalog does not exist." + } + } + if ($folder) { + if ($sourceFolders.Name -contains $folder) { + $srcFolder = $sourceFolders | Where-Object { $_.Name -eq $folder } + if ($destinationFolders.Name -contains $folder) { + if (!$force) { + Write-Message -Level Warning -Message "Integration services catalog folder $folder exists at destination. Use -Force to drop and recreate." + } + else { + if ($Pscmdlet.ShouldProcess($Destination, "Dropping folder $folder and recreating")) { + try { + New-CatalogFolder -Folder $srcFolder.Name -Description $srcFolder.Description -Force + } + catch { + Stop-Function -Message "Issue dropping folder" -Target $folder -ErrorRecord $_ + } + + } + } + } + else { + if ($Pscmdlet.ShouldProcess($Destination, "Creating folder $folder")) { + try { + New-CatalogFolder -Folder $srcFolder.Name -Description $srcFolder.Description + } + catch { + Stop-Function -Message "Issue creating folder" -Target $folder -ErrorRecord $_ + } + } + } + } + else { + throw "The source folder provided does not exist in the source Integration Services catalog." + } + } + else { + foreach ($srcFolder in $sourceFolders) { + if ($destinationFolders.Name -notcontains $srcFolder.Name) { + if ($Pscmdlet.ShouldProcess($Destination, "Creating folder $($srcFolder.Name)")) { + try { + New-CatalogFolder -Folder $srcFolder.Name -Description $srcFolder.Description + } + catch { + Stop-Function -Message "Issue creating folder" -Target $srcFolder -ErrorRecord $_ -Continue + } + } + } + else { + if (!$force) { + Write-Message -Level Warning -Message "Integration services catalog folder $($srcFolder.Name) exists at destination. Use -Force to drop and recreate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($Destination, "Dropping folder $($srcFolder.Name) and recreating")) { + try { + New-CatalogFolder -Folder $srcFolder.Name -Description $srcFolder.Description -Force + } + catch { + Stop-Function -Message "Issue dropping folder" -Target $srcFolder -ErrorRecord $_ + } + } + } + } + } + } + + # Refresh folders for project and environment deployment + if ($Pscmdlet.ShouldProcess($Destination, "Refresh folders for project deployment")) { + try { + $destinationFolders.Alter() + } + catch { + # Sometimes it says Alter() doesn't exist + } + $destinationFolders.Refresh() + } + + if ($folder) { + $sourceFolders = $sourceFolders | Where-Object { $_.Name -eq $folder } + if (!$sourceFolders) { + throw "The source folder $folder does not exist in the source Integration Services catalog." + } + } + if ($project) { + $folderDeploy = $sourceFolders | Where-Object { $_.Projects.Name -eq $project } + if (!$folderDeploy) { + throw "The project $project cannot be found in the source Integration Services catalog." + } + else { + foreach ($f in $folderDeploy) { + if ($Pscmdlet.ShouldProcess($Destination, "Deploying project $project from folder $($f.Name)")) { + try { + Invoke-ProjectDeployment -Folder $f.Name -Project $project + } + catch { + Stop-Function -Message "Issue deploying project" -Target $project -ErrorRecord $_ + } + } + } + } + } + else { + foreach ($curFolder in $sourceFolders) { + foreach ($proj in $curFolder.Projects) { + if ($Pscmdlet.ShouldProcess($Destination, "Deploying project $($proj.Name) from folder $($curFolder.Name)")) { + try { + Invoke-ProjectDeployment -Project $proj.Name -Folder $curFolder.Name + } + catch { + Stop-Function -Message "Issue deploying project" -Target $proj -ErrorRecord $_ + } + } + } + } + } + + if ($environment) { + $folderDeploy = $sourceFolders | Where-Object { $_.Environments.Name -eq $environment } + if (!$folderDeploy) { + throw "The environment $environment cannot be found in the source Integration Services catalog." + } + else { + foreach ($f in $folderDeploy) { + if ($destinationFolders[$f.Name].Environments.Name -notcontains $environment) { + if ($Pscmdlet.ShouldProcess($Destination, "Deploying environment $environment from folder $($f.Name)")) { + try { + New-FolderEnvironment -Folder $f.Name -Environment $environment + } + catch { + Stop-Function -Message "Issue deploying environment" -Target $environment -ErrorRecord $_ + } + } + } + else { + if (!$force) { + Write-Message -Level Warning -Message "Integration services catalog environment $environment exists in folder $($f.Name) at destination. Use -Force to drop and recreate." + } + else { + If ($Pscmdlet.ShouldProcess($Destination, "Dropping existing environment $environment and deploying environment $environment from folder $($f.Name)")) { + try { + New-FolderEnvironment -Folder $f.Name -Environment $environment -Force + } + catch { + Stop-Function -Message "Issue dropping existing environment" -Target $environment -ErrorRecord $_ + } + } + } + } + } + } + } + else { + foreach ($curFolder in $sourceFolders) { + foreach ($env in $curFolder.Environments) { + if ($destinationFolders[$curFolder.Name].Environments.Name -notcontains $env.Name) { + if ($Pscmdlet.ShouldProcess($Destination, "Deploying environment $($env.Name) from folder $($curFolder.Name)")) { + try { + New-FolderEnvironment -Environment $env.Name -Folder $curFolder.Name + } + catch { + Stop-Function -Message "Issue deploying environment" -Target $env -ErrorRecord $_ + } + } + } + else { + if (!$force) { + Write-Message -Level Warning -Message "Integration services catalog environment $($env.Name) exists in folder $($curFolder.Name) at destination. Use -Force to drop and recreate." + continue + } + else { + if ($Pscmdlet.ShouldProcess($Destination, "Deploying environment $($env.Name) from folder $($curFolder.Name)")) { + try { + New-FolderEnvironment -Environment $env.Name -Folder $curFolder.Name -Force + } + catch { + Stop-Function -Message "Issue deploying environment" -Target $env -ErrorRecord $_ + } + } + } + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlSsisCatalog + } +} +function Copy-DbaSysDbUserObject { + <# + .SYNOPSIS + Imports all user objects found in source SQL Server's master, msdb and model databases to the destination. + + .DESCRIPTION + Imports all user objects found in source SQL Server's master, msdb and model databases to the destination. This is useful because many DBAs store backup/maintenance procs/tables/triggers/etc (among other things) in master or msdb. + + It is also useful for migrating objects within the model database. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, SystemDatabase, UserObject + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaSysDbUserObject + + .EXAMPLE + Copy-DbaSysDbUserObject $sourceServer $destserver + + Copies user objects from source to destination + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [DbaInstanceParameter]$Source, + [PSCredential]$SourceSqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceServer.DomainInstanceName + $destination = $destServer.DomainInstanceName + + if (!(Test-SqlSa -SqlInstance $sourceServer -SqlCredential $SourceSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $source. Quitting." + return + } + if (!(Test-SqlSa -SqlInstance $destServer -SqlCredential $DestinationSqlCredential)) { + Stop-Function -Message "Not a sysadmin on $destination. Quitting." + return + } + + $systemDbs = "master", "model", "msdb" + + foreach ($systemDb in $systemDbs) { + $sysdb = $sourceServer.databases[$systemDb] + $transfer = New-Object Microsoft.SqlServer.Management.Smo.Transfer $sysdb + $transfer.CopyAllObjects = $false + $transfer.CopyAllDatabaseTriggers = $true + $transfer.CopyAllDefaults = $true + $transfer.CopyAllRoles = $true + $transfer.CopyAllRules = $true + $transfer.CopyAllSchemas = $true + $transfer.CopyAllSequences = $true + $transfer.CopyAllSqlAssemblies = $true + $transfer.CopyAllSynonyms = $true + $transfer.CopyAllTables = $true + $transfer.CopyAllViews = $true + $transfer.CopyAllStoredProcedures = $true + $transfer.CopyAllUserDefinedAggregates = $true + $transfer.CopyAllUserDefinedDataTypes = $true + $transfer.CopyAllUserDefinedTableTypes = $true + $transfer.CopyAllUserDefinedTypes = $true + $transfer.CopyAllUserDefinedFunctions = $true + $transfer.CopyAllUsers = $true + $transfer.PreserveDbo = $true + $transfer.Options.AllowSystemObjects = $false + $transfer.Options.ContinueScriptingOnError = $true + $transfer.Options.IncludeDatabaseRoleMemberships = $true + $transfer.Options.Indexes = $true + $transfer.Options.Permissions = $true + $transfer.Options.WithDependencies = $false + + Write-Message -Level Output -Message "Copying from $systemDb." + try { + $sqlQueries = $transfer.ScriptTransfer() + + foreach ($sql in $sqlQueries) { + Write-Message -Level Debug -Message $sql + if ($PSCmdlet.ShouldProcess($destServer, $sql)) { + try { + $destServer.Query($sql, $systemDb) + } + catch { + # Don't care - long story having to do with duplicate stuff + } + } + } + } + catch { + # Don't care - long story having to do with duplicate stuff + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Copy-SqlSysDbUserObjects + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Copy-DbaTableData { + <# + .SYNOPSIS + Copies data between SQL Server tables. + + .DESCRIPTION + Copies data between SQL Server tables using SQL Bulk Copy. + The same can be achieved also doing + $sourcetable = Invoke-SqlCmd2 -ServerInstance instance1 ... -As DataTable + Write-DbaDataTable -SqlInstance ... -InputObject $sourcetable + but it will force buffering the contents on the table in memory (high RAM usage for large tables). + With this function, a streaming copy will be done in the most speedy and least resource-intensive way. + + .PARAMETER SqlInstance + Source SQL Server.You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to copy the table from. + + .PARAMETER DestinationDatabase + The database to copy the table to. If not specified, it is assumed to be the same of Database + + .PARAMETER Table + Define a specific table you would like to use as source. You can specify up to three-part name like db.sch.tbl. + If the object has special characters please wrap them in square brackets [ ]. + This dbo.First.Table will try to find table named 'Table' on schema 'First' and database 'dbo'. + The correct way to find table named 'First.Table' on schema 'dbo' is passing dbo.[First.Table] + + .PARAMETER DestinationTable + The table you want to use as destination. If not specified, it is assumed to be the same of Table + + .PARAMETER Query + If you want to copy only a portion, specify the query (but please, select all the columns, or nasty things will happen) + + .PARAMETER BatchSize + The BatchSize for the import defaults to 5000. + + .PARAMETER NotifyAfter + Sets the option to show the notification after so many rows of import + + .PARAMETER NoTableLock + If this switch is enabled, a table lock (TABLOCK) will not be placed on the destination table. By default, this operation will lock the destination table while running. + + .PARAMETER CheckConstraints + If this switch is enabled, the SqlBulkCopy option to process check constraints will be enabled. + + Per Microsoft "Check constraints while data is being inserted. By default, constraints are not checked." + + .PARAMETER FireTriggers + If this switch is enabled, the SqlBulkCopy option to fire insert triggers will be enabled. + + Per Microsoft "When specified, cause the server to fire the insert triggers for the rows being inserted into the Database." + + .PARAMETER KeepIdentity + If this switch is enabled, the SqlBulkCopy option to preserve source identity values will be enabled. + + Per Microsoft "Preserve source identity values. When not specified, identity values are assigned by the destination." + + .PARAMETER KeepNulls + If this switch is enabled, the SqlBulkCopy option to preserve NULL values will be enabled. + + Per Microsoft "Preserve null values in the destination table regardless of the settings for default values. When not specified, null values are replaced by default values where applicable." + + .PARAMETER Truncate + If this switch is enabled, the destination table will be truncated after prompting for confirmation. + + .PARAMETER BulkCopyTimeOut + Value in seconds for the BulkCopy operations timeout. The default is 30 seconds. + + .PARAMETER InputObject + Enables piping of Table objects from Get-DbaTable + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration + Author: niphlod (Simone Bizzotto) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaTableData + + .EXAMPLE + Copy-DbaTableData -SqlInstance sql1 -Destination sql2 -Database dbatools_from -Table test_table + + Copies all the data from sql1 to sql2, using the database dbatools_from. + + .EXAMPLE + Copy-DbaTableData -SqlInstance sql1 -Destination sql2 -Database dbatools_from -DestinationDatabase dbatools_dest -Table test_table + + Copies all the data from sql1 to sql2, using the database dbatools_from as source and dbatools_dest as destination + + .EXAMPLE + Get-DbaTable -SqlInstance sql1 -Database tempdb -Table tb1, tb2 | Copy-DbaTableData -DestinationTable tb3 + + Copies all data from tables tb1 and tb2 in tempdb on sql1 to tb3 in tempdb onsql1 + + .EXAMPLE + Get-DbaTable -SqlInstance sql1 -Database tempdb -Table tb1, tb2 | Copy-DbaTableData -Destination sql2 + + Copies data from tbl1 in tempdb on sql1 to tbl1 in tempdb on sql2 + then + Copies data from tbl2 in tempdb on sql1 to tbl2 in tempdb on sql2 + + .EXAMPLE + Copy-DbaTableData -SqlInstance sql1 -Destination sql2 -Database dbatools_from -Table test_table + + Copies all the data from sql1 to sql2, using the database dbatools_from. + + .EXAMPLE + Copy-DbaTableData -SqlInstance sql1 -Destination sql2 -Database dbatools_from -Table test_table -KeepIdentity -Truncate + + Copies all the data from sql1 to sql2, using the database dbatools_from, keeping identity columns and truncating the destination + + .EXAMPLE + Copy-DbaTableData -SqlInstance sql1 -Destination sql2 -Database dbatools_from -Table test_table -KeepIdentity -Truncate + + Copies all the data from sql1 to sql2, using the database dbatools_from, keeping identity columns and truncating the destination + + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Alias("ServerInstance", "SqlServer", "Source")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [DbaInstanceParameter]$Destination, + [PSCredential]$DestinationSqlCredential, + [string]$Database, + [string]$DestinationDatabase, + [string[]]$Table, + [string]$Query, + [int]$BatchSize = 50000, + [int]$NotifyAfter = 5000, + [string]$DestinationTable, + [switch]$NoTableLock, + [switch]$CheckConstraints, + [switch]$FireTriggers, + [switch]$KeepIdentity, + [switch]$KeepNulls, + [switch]$Truncate, + [int]$bulkCopyTimeOut = 5000, + [Parameter(ValueFromPipeline)] + [Microsoft.SqlServer.Management.Smo.Table[]]$InputObject, + [switch]$EnableException + ) + + begin { + # Getting the total rows copied is a challenge. Use SqlBulkCopyExtension. + # http://stackoverflow.com/questions/1188384/sqlbulkcopy-row-count-when-complete + + $sourcecode = 'namespace System.Data.SqlClient { + using Reflection; + + public static class SqlBulkCopyExtension + { + const String _rowsCopiedFieldName = "_rowsCopied"; + static FieldInfo _rowsCopiedField = null; + + public static int RowsCopiedCount(this SqlBulkCopy bulkCopy) + { + if (_rowsCopiedField == null) _rowsCopiedField = typeof(SqlBulkCopy).GetField(_rowsCopiedFieldName, BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance); + return (int)_rowsCopiedField.GetValue(bulkCopy); + } + } + }' + + Add-Type -ReferencedAssemblies System.Data.dll -TypeDefinition $sourcecode -ErrorAction SilentlyContinue + $bulkCopyOptions = 0 + $options = "TableLock", "CheckConstraints", "FireTriggers", "KeepIdentity", "KeepNulls", "Default" + + foreach ($option in $options) { + $optionValue = Get-Variable $option -ValueOnly -ErrorAction SilentlyContinue + if ($option -eq "TableLock" -and (!$NoTableLock)) { + $optionValue = $true + } + if ($optionValue -eq $true) { + $bulkCopyOptions += $([Data.SqlClient.SqlBulkCopyOptions]::$option).value__ + } + } + } + + process { + if ((Test-Bound -Not -ParameterName Table, SqlInstance) -and (Test-Bound -Not -ParameterName InputObject)) { + Stop-Function -Message "You must pipe in a table or specify SqlInstance, Database and Table." + return + } + + if ($SqlInstance) { + if ((Test-Bound -Not -ParameterName Database)) { + Stop-Function -Message "Database is required when passing a SqlInstance" -Target $Table + return + } + + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $SqlInstance + return + } + + if ($Database -notin $server.Databases.Name) { + Stop-Function -Message "Database $Database doesn't exist on $server" + return + } + + try { + $InputObject += Get-DbaTable -SqlInstance $server -Table $Table -Database $Database -EnableException -Verbose:$false + } + catch { + Stop-Function -Message "Unable to determine source table : $Table" + return + } + } + + foreach ($sqltable in $InputObject) { + $Database = $sqltable.Parent.Name + $server = $sqltable.Parent.Parent + + if ((Test-Bound -Not -ParameterName DestinationDatabase)) { + $DestinationDatabase = $Database + } + + if ((Test-Bound -Not -ParameterName DestinationTable)) { + $DestinationTable = $sqltable.Name + } + + if ((Test-Bound -Not -ParameterName Destination)) { + $destServer = $server + } + else { + try { + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $Destination + return + } + } + + if ($DestinationDatabase -notin $destServer.Databases.Name) { + Stop-Function -Message "Database $DestinationDatabase doesn't exist on $destServer" + return + } + + try { + $desttable = Get-DbaTable -SqlInstance $destServer -Table $DestinationTable -Database $DestinationDatabase -EnableException -Verbose:$false | Select-Object -First 1 + } + catch { + Stop-Function -Message "Unable to determine destination table: $DestinationTable" + return + } + + if (-not $desttable) { + Stop-Function -Message "$DestinationTable does not exist on destination" + return + } + + $connstring = $destServer.ConnectionContext.ConnectionString + + $fqtnfrom = "$($server.Databases[$Database]).$sqltable" + $fqtndest = "$($destServer.Databases[$DestinationDatabase]).$desttable" + + if (Test-Bound -ParameterName Query -Not) { + $Query = "SELECT * FROM $fqtnfrom" + } + try { + if ($Truncate -eq $true) { + if ($Pscmdlet.ShouldProcess($destServer, "Truncating table $fqtndest")) { + $null = $destServer.Databases[$DestinationDatabase].ExecuteNonQuery("TRUNCATE TABLE $fqtndest") + } + } + $cmd = $server.ConnectionContext.SqlConnectionObject.CreateCommand() + $cmd.CommandText = $Query + if ($server.ConnectionContext.IsOpen -eq $false) { + $server.ConnectionContext.SqlConnectionObject.Open() + } + $bulkCopy = New-Object Data.SqlClient.SqlBulkCopy("$connstring;Database=$DestinationDatabase", $bulkCopyOptions) + $bulkCopy.DestinationTableName = $fqtndest + $bulkCopy.EnableStreaming = $true + $bulkCopy.BatchSize = $BatchSize + $bulkCopy.NotifyAfter = $NotifyAfter + $bulkCopy.BulkCopyTimeOut = $BulkCopyTimeOut + + $elapsed = [System.Diagnostics.Stopwatch]::StartNew() + # Add RowCount output + $bulkCopy.Add_SqlRowsCopied({ + $RowsPerSec = [math]::Round($args[1].RowsCopied / $elapsed.ElapsedMilliseconds * 1000.0, 1) + Write-Progress -id 1 -activity "Inserting rows" -Status ([System.String]::Format("{0} rows ({1} rows/sec)", $args[1].RowsCopied, $RowsPerSec)) + }) + + if ($Pscmdlet.ShouldProcess($destServer, "Writing rows to $fqtndest")) { + $reader = $cmd.ExecuteReader() + $bulkCopy.WriteToServer($reader) + $RowsTotal = [System.Data.SqlClient.SqlBulkCopyExtension]::RowsCopiedCount($bulkCopy) + $TotalTime = [math]::Round($elapsed.Elapsed.TotalSeconds, 1) + Write-Message -Level Verbose -Message "$RowsTotal rows inserted in $TotalTime sec" + if ($rowCount -is [int]) { + Write-Progress -id 1 -activity "Inserting rows" -status "Complete" -Completed + } + } + + $bulkCopy.Close() + $bulkCopy.Dispose() + $reader.Close() + + [pscustomobject]@{ + SourceInstance = $server.Name + SourceDatabase = $Database + SourceTable = $sqltable.Name + DestinationInstance = $destServer.name + DestinationDatabase = $DestinationDatabase + DestinationTable = $desttable.Name + RowsCopied = $rowstotal + Elapsed = [prettytimespan]$elapsed.Elapsed + } + } + catch { + Stop-Function -Message "Something went wrong" -ErrorRecord $_ -Target $server -continue + } + } + } +} +function Copy-DbaXESessionTemplate { + <# + .SYNOPSIS + Copies non-Microsoft templates from the dbatools template repository (\bin\xetemplates\) to $home\Documents\SQL Server Management Studio\Templates\XEventTemplates. + + .DESCRIPTION + Copies non-Microsoft templates from the dbatools template repository (\bin\xetemplates\) to $home\Documents\SQL Server Management Studio\Templates\XEventTemplates. + + Useful for when you want to use the SSMS GUI. + + .PARAMETER Path + The path to the template directory. Defaults to the dbatools template repository (\bin\xetemplates\). + + .PARAMETER Destination + Path to the Destination directory, defaults to $home\Documents\SQL Server Management Studio\Templates\XEventTemplates. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Copy-DbaXESessionTemplate + + .EXAMPLE + Copy-DbaXESessionTemplate + + Copies non-Microsoft templates from the dbatools template repository (\bin\xetemplates\) to $home\Documents\SQL Server Management Studio\Templates\XEventTemplates. + + .EXAMPLE + Copy-DbaXESessionTemplate -Path C:\temp\xetemplates + + Copies your templates from C:\temp\xetemplates to $home\Documents\SQL Server Management Studio\Templates\XEventTemplates. + + #> + [CmdletBinding()] + param ( + [string[]]$Path = "$script:PSModuleRoot\bin\xetemplates", + [string]$Destination = "$home\Documents\SQL Server Management Studio\Templates\XEventTemplates", + [switch]$EnableException + ) + process { + if (-not (Test-Path -Path $Destination)) { + try { + $null = New-Item -ItemType Directory -Path $Destination -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $Destination + } + } + try { + $files = (Get-DbaXESessionTemplate -Path $Path | Where-Object Source -ne Microsoft).Path + foreach ($file in $files) { + Write-Message -Level Output -Message "Copying $($file.Name) to $Destination." + Copy-Item -Path $file -Destination $Destination -ErrorAction Stop + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $path + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Disable-DbaAgHadr { + <# + .SYNOPSIS + Disables the Hadr service setting on the specified SQL Server. + + .DESCRIPTION + In order to build an AG a cluster has to be built and then the Hadr enabled for the SQL Server + service. This function disables that feature for the SQL Server service. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER Credential + Credential object used to connect to the Windows server itself as a different user + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + Will restart SQL Server and SQL Server Agent service to apply the change. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Disable-DbaAgHadr + + .EXAMPLE + Disable-DbaAgHadr -SqlInstance sql2016 -Force + + Sets Hadr service to disabled for the instance sql2016, and restart the service to apply the change. + + .EXAMPLE + Disable-DbaAgHadr -SqlInstance sql2012\dev1 -Force + + Sets Hadr service to disabled for the instance dev1 on sq2012, and restart the service to apply the change. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$Credential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function GetDbaAgHadr { + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + + try { + $computer = $computerName = $instance.ComputerName + $instanceName = $instance.InstanceName + Write-Message -Level Verbose -Message "Attempting to connect to $computer" + $currentState = Invoke-ManagedComputerCommand -ComputerName $computerName -ScriptBlock { $wmi.Services[$args[0]] | Select-Object IsHadrEnabled } -ArgumentList $instanceName -Credential $Credential + } + catch { + Stop-Function -Message "Failure connecting to $computer" -Category ConnectionError -ErrorRecord $_ -Target $instance + return + } + + if ($null -eq $currentState.IsHadrEnabled) { + $isenabled = $false + } + else { + $isenabled = $currentState.IsHadrEnabled + } + [PSCustomObject]@{ + ComputerName = $computer + InstanceName = $instanceName + SqlInstance = $instance.FullName + IsHadrEnabled = $isenabled + } + } + } + } + } + + process { + foreach ($instance in $SqlInstance) { + $computer = $computerFullName = $instance.ComputerName + $instanceName = $instance.InstanceName + if (-not (Test-ElevationRequirement -ComputerName $instance)) { + return + } + $noChange = $false + + switch ($instance.InstanceName) { + 'MSSQLSERVER' { $agentName = 'SQLSERVERAGENT' } + default { $agentName = "SQLAgent`$$instanceName" } + } + + try { + Write-Message -Level Verbose -Message "Checking current Hadr setting for $computer" + $currentState = GetDbaAgHadr -SqlInstance $instance -Credential $Credential + } + catch { + Stop-Function -Message "Failure to pull current state of Hadr setting on $computer" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $isHadrEnabled = $currentState.IsHadrEnabled + Write-Message -Level InternalComment -Message "$instance Hadr current value: $isHadrEnabled" + + # hadr results from sql wmi can be iffy, skip the check + <# + if (-not $isHadrEnabled) { + Write-Message -Level Warning -Message "Hadr is already disabled for instance: $($instance.FullName)" + $noChange = $true + continue + } + #> + + $scriptblock = { + $instance = $args[0] + $sqlService = $wmi.Services | Where-Object DisplayName -eq "SQL Server ($instance)" + $sqlService.ChangeHadrServiceSetting(0) + } + + if ($noChange -eq $false) { + if ($PSCmdlet.ShouldProcess($instance, "Changing Hadr from $isHadrEnabled to 0 for $instance")) { + try { + Invoke-ManagedComputerCommand -ComputerName $computerFullName -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $instancename + } + catch { + Stop-Function -Continue -Message "Failure on $($instance.FullName) | This may be because AlwaysOn Availability Groups feature requires the x86(non-WOW) or x64 Enterprise Edition of SQL Server 2012 (or later version) running on Windows Server 2008 (or later version) with WSFC hotfix KB 2494036 installed." + } + } + if (Test-Bound 'Force') { + if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { + try { + $null = Stop-DbaSqlService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + $null = Start-DbaSqlService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + } + catch { + Stop-Function -Message "Issue restarting $instance" -Target $instance -Continue + } + } + } + $newState = GetDbaAgHadr -SqlInstance $instance -Credential $Credential + + if (Test-Bound -Not -ParameterName Force) { + Write-Message -Level Warning -Message "You must restart the SQL Server for it to take effect." + } + + [PSCustomObject]@{ + ComputerName = $newState.ComputerName + InstanceName = $newState.InstanceName + SqlInstance = $newState.SqlInstance + IsHadrEnabled = $false + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Disable-DbaForceNetworkEncryption { + <# + .SYNOPSIS + Disables Force Encryption for a SQL Server instance + + .DESCRIPTION + Disables Force Encryption for a SQL Server instance. Note that this requires access to the Windows Server, not the SQL instance itself. + + This setting is found in Configuration Manager. + + .PARAMETER SqlInstance + The target SQL Server. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to the computer (not SQL Server instance) using alternative Windows credentials. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Disable-DbaForceNetworkEncryption + + Disables Force Encryption on the default (MSSQLSERVER) instance on localhost - requires (and checks for) RunAs admin. + + .EXAMPLE + Disable-DbaForceNetworkEncryption -SqlInstance sql01\SQL2008R2SP2 + + Disables Force Network Encryption for the SQL2008R2SP2 on sql01. Uses Windows Credentials to both login and modify the registry. + + .EXAMPLE + Disable-DbaForceNetworkEncryption -SqlInstance sql01\SQL2008R2SP2 -WhatIf + + Shows what would happen if the command were executed. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance." -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname." + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance." -Target $instance -Continue -Category InvalidArgument + } + + Write-Message -Level Output -Message "Connecting to SQL WMI on $($instance.ComputerName)." + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FullComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($($instance.InstanceName))" + } + catch { + Stop-Function -Message "Failed to access $instance." -Target $instance -Continue -ErrorRecord $_ + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + try { + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + } + catch { + # Probably because the instance name has been aliased or does not exist or samthin + } + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance." -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regpath = "Registry::HKEY_LOCAL_MACHINE\$($args[0])\MSSQLServer\SuperSocketNetLib" + $cert = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + $oldvalue = (Get-ItemProperty -Path $regpath -Name ForceEncryption).ForceEncryption + Set-ItemProperty -Path $regpath -Name ForceEncryption -Value $false + $forceencryption = (Get-ItemProperty -Path $regpath -Name ForceEncryption).ForceEncryption + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $args[2] + SqlInstance = $args[1] + ForceEncryption = ($forceencryption -eq $true) + CertificateThumbprint = $cert + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $instance to modify the ForceEncryption value in $regroot for $($instance.InstanceName)")) { + try { + Invoke-Command2 -ComputerName $resolved.FullComputerName -Credential $Credential -ArgumentList $regroot, $vsname, $instancename -ScriptBlock $scriptblock -ErrorAction Stop + Write-Message -Level Critical -Message "Force encryption was successfully set on $($resolved.FullComputerName) for the $instancename instance. You must now restart the SQL Server for changes to take effect." -Target $instance + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.FullComputerName) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} +function Disable-DbaTraceFlag { + <# + .SYNOPSIS + Disable a Global Trace Flag that is currently running + + .DESCRIPTION + The function will disable a Trace Flag that is currently running globally on the SQL Server instance(s) listed + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER TraceFlag + Trace flag number to enable globally + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: TraceFlag + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Disable-DbaTraceFlag + + .EXAMPLE + Disable-DbaTraceFlag -SqlInstance sql2016 -TraceFlag 3226 + Disable the globally running trace flag 3226 on SQL Server instance sql2016 +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [int[]]$TraceFlag, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $current = Get-DbaTraceFlag -SqlInstance $server -EnableException + + foreach ($tf in $TraceFlag) { + $TraceFlagInfo = [pscustomobject]@{ + SourceServer = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TraceFlag = $tf + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + if ($tf -notin $current.TraceFlag) { + $TraceFlagInfo.Status = 'Skipped' + $TraceFlagInfo.Notes = "Trace Flag is not running." + $TraceFlagInfo + Write-Message -Level Warning -Message "Trace Flag $tf is not currently running on $instance" + continue + } + + try { + $query = "DBCC TRACEOFF ($tf, -1)" + $server.Query($query) + } + catch { + $TraceFlagInfo.Status = "Failed" + $TraceFlagInfo.Notes = $_.Exception.Message + $TraceFlagInfo + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + $TraceFlagInfo.Status = "Successful" + $TraceFlagInfo + } + } + } +} +function Dismount-DbaDatabase { + <# + .SYNOPSIS + Detach a SQL Server Database. + + .DESCRIPTION + This command detaches one or more SQL Server databases. If necessary, -Force can be used to break mirrors and remove databases from availability groups prior to detaching. + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to detach. + + .PARAMETER FileStructure + A StringCollection object value that contains a list database files. If FileStructure is not specified, BackupHistory will be used to guess the structure. + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase), to be detached. + + .PARAMETER UpdateStatistics + If this switch is enabled, statistics for the database will be updated prior to detaching it. + + .PARAMETER Force + If this switch is enabled and the database is part of a mirror, the mirror will be broken. If the database is part of an Availability Group, it will be removed from the AG. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Dismount-DbaDatabase + + .EXAMPLE + Detach-DbaDatabase -SqlInstance sql2016b -Database SharePoint_Config, WSS_Logging + + Detaches SharePoint_Config and WSS_Logging from sql2016b + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2016b -Database 'PerformancePoint Service Application_10032db0fa0041df8f913f558a5dc0d4' | Detach-DbaDatabase -Force + + Detaches 'PerformancePoint Service Application_10032db0fa0041df8f913f558a5dc0d4' from sql2016b. Since Force was specified, if the database is part of mirror, the mirror will be broken prior to detaching. + + If the database is part of an Availability Group, it will first be dropped prior to detachment. + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2016b -Database WSS_Logging | Detach-DbaDatabase -Force -WhatIf + + Shows what would happen if the command were to execute (without actually executing the detach/break/remove commands). + + #> + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory, ParameterSetName = 'SqlInstance')] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = 'SqlInstance')] + [string]$Database, + [parameter(Mandatory, ParameterSetName = 'Pipeline', ValueFromPipeline)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [Switch]$UpdateStatistics, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $InputObject += $server.Databases | Where-Object Name -in $Database + } + else { + $InputObject += $server.Databases + } + + if ($ExcludeDatabase) { + $InputObject = $InputObject | Where-Object Name -NotIn $ExcludeDatabase + } + } + + foreach ($db in $InputObject) { + $db.Refresh() + $server = $db.Parent + + if ($db.IsSystemObject) { + Stop-Function -Message "$db is a system database and cannot be detached using this method." -Target $db -Continue + } + + Write-Message -Level Verbose -Message "Checking replication status." + if ($db.ReplicationOptions -ne "None") { + Stop-Function -Message "Skipping $db on $server because it is replicated." -Target $db -Continue + } + + # repeat because different servers could be piped in + $snapshots = (Get-DbaDbSnapshot -SqlInstance $server).SnapshotOf + Write-Message -Level Verbose -Message "Checking for snaps" + if ($db.Name -in $snapshots) { + Write-Message -Level Warning -Message "Database $db has snapshots, you need to drop them before detaching. Skipping $db on $server." + Continue + } + + Write-Message -Level Verbose -Message "Checking mirror status" + if ($db.IsMirroringEnabled -and !$Force) { + Stop-Function -Message "$db on $server is being mirrored. Use -Force to break mirror or use the safer backup/restore method." -Target $db -Continue + } + + Write-Message -Level Verbose -Message "Checking Availability Group status" + + if ($db.AvailabilityGroupName -and !$Force) { + $ag = $db.AvailabilityGroupName + Stop-Function -Message "$db on $server is part of an Availability Group ($ag). Use -Force to drop from $ag availability group to detach. Alternatively, you can use the safer backup/restore method." -Target $db -Continue + } + + $sessions = Get-DbaProcess -SqlInstance $db.Parent -Database $db.Name + + if ($sessions -and !$Force) { + Stop-Function -Message "$db on $server currently has connected users and cannot be dropped. Use -Force to kill all connections and detach the database." -Target $db -Continue + } + + if ($force) { + + if ($sessions) { + If ($Pscmdlet.ShouldProcess($server, "Killing $($sessions.count) sessions which are connected to $db")) { + $null = $sessions | Stop-DbaProcess -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + } + + if ($db.IsMirroringEnabled) { + If ($Pscmdlet.ShouldProcess($server, "Breaking mirror for $db on $server")) { + try { + Write-Message -Level Warning -Message "Breaking mirror for $db on $server." + $db.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $db.Alter() + $db.Refresh() + } + catch { + Stop-Function -Message "Could not break mirror for $db on $server - not detaching." -Target $db -ErrorRecord $_ -Continue + } + } + } + + if ($db.AvailabilityGroupName) { + $ag = $db.AvailabilityGroupName + If ($Pscmdlet.ShouldProcess($server, "Attempting remove $db on $server from Availability Group $ag")) { + try { + $server.AvailabilityGroups[$ag].AvailabilityDatabases[$db.name].Drop() + Write-Message -Level Verbose -Message "Successfully removed $db from detach from $ag on $server." + } + catch { + if ($_.Exception.InnerException) { + $exception = $_.Exception.InnerException.ToString() -Split "System.Data.SqlClient.SqlException: " + $exception = " | $(($exception[1] -Split "at Microsoft.SqlServer.Management.Common.ConnectionManager")[0])".TrimEnd() + } + + Stop-Function -Message "Could not remove $db from $ag on $server $exception." -Target $db -ErrorRecord $_ -Continue + } + } + } + + $sessions = Get-DbaProcess -SqlInstance $db.Parent -Database $db.Name + + if ($sessions) { + If ($Pscmdlet.ShouldProcess($server, "Killing $($sessions.count) sessions which are still connected to $db")) { + $null = $sessions | Stop-DbaProcess -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + } + } + + If ($Pscmdlet.ShouldProcess($server, "Detaching $db on $server")) { + try { + $server.DetachDatabase($db.Name, $UpdateStatistics) + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + DetachResult = "Success" + } + } + catch { + Stop-Function -Message "Failure" -Target $db -ErrorRecord $_ -Continue + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Enable-DbaAgHadr { + <# + .SYNOPSIS + Enables the Hadr service setting on the specified SQL Server. + + .DESCRIPTION + In order to build an AG a cluster has to be built and then the Hadr enabled for the SQL Server + service. This function enables that feature for the SQL Server service. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER Credential + Credential object used to connect to the Windows server itself as a different user + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + Will restart SQL Server and SQL Server Agent service to apply the change. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Enable-DbaAgHadr + + .EXAMPLE + Enable-DbaAgHadr -SqlInstance sql2016 -Force + + Sets Hadr service to enabled for the instance sql2016, and restart the service to apply the change. + + .EXAMPLE + Enable-DbaAgHadr -SqlInstance sql2012\dev1 -Force + + Sets Hadr service to disabled for the instance dev1 on sq2012, and restart the service to apply the change. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$Credential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function GetDbaAgHadr { + <# + .SYNOPSIS + Gets the Hadr service setting on the specified SQL Server instance. + + .DESCRIPTION + Gets the Hadr setting, from the service level, and returns true or false for the specified SQL Server instance. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER Credential + Credential object used to connect to the Windows server itself as a different user + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/GetDbaAgHadr + + .EXAMPLE + GetDbaAgHadr -SqlInstance sql2016 + + Returns a status of the Hadr setting for sql2016 SQL Server instance. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + + try { + $computer = $computerName = $instance.ComputerName + $instanceName = $instance.InstanceName + Write-Message -Level Verbose -Message "Attempting to connect to $computer" + $currentState = Invoke-ManagedComputerCommand -ComputerName $computerName -ScriptBlock { $wmi.Services[$args[0]] | Select-Object IsHadrEnabled } -ArgumentList $instanceName -Credential $Credential + } + catch { + Stop-Function -Message "Failure connecting to $computer" -Category ConnectionError -ErrorRecord $_ -Target $instance + return + } + + if ($null -eq $currentState.IsHadrEnabled) { + $isenabled = $false + } + else { + $isenabled = $currentState.IsHadrEnabled + } + [PSCustomObject]@{ + ComputerName = $computer + InstanceName = $instanceName + SqlInstance = $instance.FullName + IsHadrEnabled = $isenabled + } + } + } + } + } + process { + foreach ($instance in $SqlInstance) { + $computer = $computerFullName = $instance.ComputerName + $instanceName = $instance.InstanceName + if (-not (Test-ElevationRequirement -ComputerName $instance)) { + return + } + $noChange = $false + + switch ($instance.InstanceName) { + 'MSSQLSERVER' { $agentName = 'SQLSERVERAGENT' } + default { $agentName = "SQLAgent`$$instanceName" } + } + + try { + Write-Message -Level Verbose -Message "Checking current Hadr setting for $computer" + $currentState = GetDbaAgHadr -SqlInstance $instance -Credential $Credential + } + catch { + Stop-Function -Message "Failure to pull current state of Hadr setting on $computer" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $isHadrEnabled = $currentState.IsHadrEnabled + Write-Message -Level InternalComment -Message "$instance Hadr current value: $isHadrEnabled" + + # hadr results from sql wmi can be iffy, skip the check + <# + if ($isHadrEnabled) { + Write-Message -Level Warning -Message "Hadr is already enabled for instance: $($instance.FullName)" + $noChange = $true + continue + } + #> + + $scriptblock = { + $instance = $args[0] + $sqlService = $wmi.Services | Where-Object DisplayName -eq "SQL Server ($instance)" + $sqlService.ChangeHadrServiceSetting(1) + } + + if ($noChange -eq $false) { + if ($PSCmdlet.ShouldProcess($instance, "Changing Hadr from $isHadrEnabled to 1 for $instance")) { + try { + Invoke-ManagedComputerCommand -ComputerName $computerFullName -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $instancename + } + catch { + Stop-Function -Continue -Message "Failure on $($instance.FullName) | This may be because AlwaysOn Availability Groups feature requires the x86(non-WOW) or x64 Enterprise Edition of SQL Server 2012 (or later version) running on Windows Server 2008 (or later version) with WSFC hotfix KB 2494036 installed." + } + } + } + + if (Test-Bound -ParameterName Force) { + if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { + try { + $null = Stop-DbaSqlService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + $null = Start-DbaSqlService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + } + catch { + Stop-Function -Message "Issue restarting $instance" -Target $instance -Continue + } + } + } + $newState = GetDbaAgHadr -SqlInstance $instance -Credential $Credential + + if (Test-Bound -Not -ParameterName Force) { + Write-Message -Level Warning -Message "You must restart the SQL Server for it to take effect." + } + + [PSCustomObject]@{ + ComputerName = $newState.ComputerName + InstanceName = $newState.InstanceName + SqlInstance = $newState.SqlInstance + IsHadrEnabled = $true + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Enable-DbaForceNetworkEncryption { + <# + .SYNOPSIS + Enables Force Encryption for a SQL Server instance. + + .DESCRIPTION + Enables Force Encryption for a SQL Server instance. Note that this requires access to the Windows Server, not the SQL instance itself. + + This setting is found in Configuration Manager. + + .PARAMETER SqlInstance + The target SQL Server. + + .PARAMETER Credential + Allows you to login to the computer (not SQL Server instance) using alternative Windows credentials + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Enable-DbaForceNetworkEncryption + + Enables Force Encryption on the default (MSSQLSERVER) instance on localhost. Requires (and checks for) RunAs admin. + + .EXAMPLE + Enable-DbaForceNetworkEncryption -SqlInstance sql01\SQL2008R2SP2 + + Enables Force Network Encryption for the SQL2008R2SP2 on sql01. Uses Windows Credentials to both connect and modify the registry. + + .EXAMPLE + Enable-DbaForceNetworkEncryption -SqlInstance sql01\SQL2008R2SP2 -WhatIf + + Shows what would happen if the command were executed. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low", DefaultParameterSetName = 'Default')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance." -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname." + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance." -Target $instance -Continue -Category InvalidArgument + } + + Write-Message -Level Output -Message "Connecting to SQL WMI on $($instance.ComputerName)." + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FullComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($($instance.InstanceName))" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + try { + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + } + catch { + # Probably because the instance name has been aliased or does not exist or samthin + } + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance." -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regpath = "Registry::HKEY_LOCAL_MACHINE\$($args[0])\MSSQLServer\SuperSocketNetLib" + $cert = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + $oldvalue = (Get-ItemProperty -Path $regpath -Name ForceEncryption).ForceEncryption + Set-ItemProperty -Path $regpath -Name ForceEncryption -Value $true + $forceencryption = (Get-ItemProperty -Path $regpath -Name ForceEncryption).ForceEncryption + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $args[2] + SqlInstance = $args[1] + ForceEncryption = ($forceencryption -eq $true) + CertificateThumbprint = $cert + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $instance to modify the ForceEncryption value in $regroot for $($instance.InstanceName)")) { + try { + Invoke-Command2 -ComputerName $resolved.FullComputerName -Credential $Credential -ArgumentList $regroot, $vsname, $instancename -ScriptBlock $scriptblock -ErrorAction Stop | Select-Object -Property * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName + Write-Message -Level Critical -Message "Force encryption was successfully set on $($resolved.FullComputerName) for the $instancename instance. You must now restart the SQL Server for changes to take effect." -Target $instance + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.FullComputerName) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} +function Enable-DbaTraceFlag { + <# + .SYNOPSIS + Enable Global Trace Flag(s) + .DESCRIPTION + The function will set one or multiple trace flags on the SQL Server instance(s) listed + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER TraceFlag + Trace flag number(s) to enable globally + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: TraceFlag + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Enable-DbaTraceFlag + + .EXAMPLE + Enable-DbaTraceFlag -SqlInstance sql2016 -TraceFlag 3226 + Enable the trace flag 3226 on SQL Server instance sql2016 + + .EXAMPLE + Enable-DbaTraceFlag -SqlInstance sql2016 -TraceFlag 1117, 1118 + Enable multiple trace flags on SQL Server instance sql2016 +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [int[]]$TraceFlag, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $CurrentRunningTraceFlags = Get-DbaTraceFlag -SqlInstance $server -EnableException + + # We could combine all trace flags but the granularity is worth it + foreach ($tf in $TraceFlag) { + $TraceFlagInfo = [pscustomobject]@{ + SourceServer = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TraceFlag = $tf + Status = $null + Notes = $null + DateTime = [DbaDateTime](Get-Date) + } + If ($CurrentRunningTraceFlags.TraceFlag -contains $tf) { + $TraceFlagInfo.Status = 'Skipped' + $TraceFlagInfo.Notes = "The Trace flag is already running." + $TraceFlagInfo + Write-Message -Level Warning -Message "The Trace flag [$tf] is already running globally." + continue + } + + try { + $query = "DBCC TRACEON ($tf, -1)" + $server.Query($query) + $server.Refresh() + } + catch { + $TraceFlagInfo.Status = "Failed" + $TraceFlagInfo.Notes = $_.Exception.Message + $TraceFlagInfo + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + $TraceFlagInfo.Status = "Successful" + $TraceFlagInfo + } + } + } +} +function Expand-DbaTLogResponsibly { + <# + .SYNOPSIS + This command will help you to automatically grow your transaction log file in a responsible way (preventing the generation of too many VLFs). + + .DESCRIPTION + As you may already know, having a transaction log file with too many Virtual Log Files (VLFs) can hurt your database performance in many ways. + + Example: + Too many VLFs can cause transaction log backups to slow down and can also slow down database recovery and, in extreme cases, even impact insert/update/delete performance. + + References: + http://www.sqlskills.com/blogs/kimberly/transaction-log-vlfs-too-many-or-too-few/ + http://blogs.msdn.com/b/saponsqlserver/archive/2012/02/22/too-many-virtual-log-files-vlfs-can-cause-slow-database-recovery.aspx + http://www.brentozar.com/blitz/high-virtual-log-file-vlf-count/ + + In order to get rid of this fragmentation we need to grow the file taking the following into consideration: + - How many VLFs are created when we perform a grow operation or when an auto-grow is invoked? + + Note: In SQL Server 2014 this algorithm has changed (http://www.sqlskills.com/blogs/paul/important-change-vlf-creation-algorithm-sql-server-2014/) + + Attention: + We are growing in MB instead of GB because of known issue prior to SQL 2012: + More detail here: + http://www.sqlskills.com/BLOGS/PAUL/post/Bug-log-file-growth-broken-for-multiples-of-4GB.aspx + and + http://connect.microsoft.com/SqlInstance/feedback/details/481594/log-growth-not-working-properly-with-specific-growth-sizes-vlfs-also-not-created-appropriately + or + https://connect.microsoft.com/SqlInstance/feedback/details/357502/transaction-log-file-size-will-not-grow-exactly-4gb-when-filegrowth-4gb + + Understanding related problems: + http://www.sqlskills.com/blogs/kimberly/transaction-log-vlfs-too-many-or-too-few/ + http://blogs.msdn.com/b/saponsqlserver/archive/2012/02/22/too-many-virtual-log-files-vlfs-can-cause-slow-database-recovery.aspx + http://www.brentozar.com/blitz/high-virtual-log-file-vlf-count/ + + Known bug before SQL Server 2012 + http://www.sqlskills.com/BLOGS/PAUL/post/Bug-log-file-growth-broken-for-multiples-of-4GB.aspx + http://connect.microsoft.com/SqlInstance/feedback/details/481594/log-growth-not-working-properly-with-specific-growth-sizes-vlfs-also-not-created-appropriately + https://connect.microsoft.com/SqlInstance/feedback/details/357502/transaction-log-file-size-will-not-grow-exactly-4gb-when-filegrowth-4gb + + How it works? + The transaction log will grow in chunks until it reaches the desired size. + Example: If you have a log file with 8192MB and you say that the target size is 81920MB (80GB) it will grow in chunks of 8192MB until it reaches 81920MB. 8192 -> 16384 -> 24576 ... 73728 -> 81920 + + .PARAMETER SqlInstance + The target SQL Server instance. + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER TargetLogSizeMB + Specifies the target size of the transaction log file in megabytes. + + .PARAMETER IncrementSizeMB + Specifies the amount the transaction log should grow in megabytes. If this value differs from the suggested value based on your TargetLogSizeMB, you will be prompted to confirm your choice. + + This value will be calculated if not specified. + + .PARAMETER LogFileId + Specifies the file number(s) of additional transaction log files to grow. + + If this value is not specified, only the first transaction log file will be processed. + + .PARAMETER ShrinkLogFile + If this switch is enabled, your transaction log files will be shrunk. + + .PARAMETER ShrinkSizeMB + Specifies the target size of the transaction log file for the shrink operation. + + .PARAMETER BackupDirectory + Specifies the location of your backups. Backups must be performed to shrink the transaction log. + + If this value is not specified, the SQL Server instance's default backup directory will be used. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ExcludeDatabase + The database(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Storage, Backup + This script uses Get-DbaDiskSpace dbatools command to get the TLog's drive free space + + Author: Claudio Silva (@ClaudioESSilva) + Requires: ALTER DATABASE permission + Limitations: Freespace cannot be validated on the directory where the log file resides in SQL Server 2005. + + Website: https://dbatools.io + Copyright (C) 2016 Chrissy LeMaire + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Expand-DbaTLogResponsibly + + .EXAMPLE + Expand-DbaTLogResponsibly -SqlInstance sqlcluster -Database db1 -TargetLogSizeMB 50000 + + Grows the transaction log for database db1 on sqlcluster to 50000 MB and calculates the increment size. + + .EXAMPLE + Expand-DbaTLogResponsibly -SqlInstance sqlcluster -Database db1, db2 -TargetLogSizeMB 10000 -IncrementSizeMB 200 + + Grows the transaction logs for databases db1 and db2 on sqlcluster to 1000MB and sets the growth increment to 200MB. + + .EXAMPLE + Expand-DbaTLogResponsibly -SqlInstance sqlcluster -Database db1 -TargetLogSizeMB 10000 -LogFileId 9 + + Grows the transaction log file with FileId 9 of the db1 database on sqlcluster instance to 10000MB. + + .EXAMPLE + Expand-DbaTLogResponsibly -SqlInstance sqlcluster -Database (Get-Content D:\DBs.txt) -TargetLogSizeMB 50000 + + Grows the transaction log of the databases specified in the file 'D:\DBs.txt' on sqlcluster instance to 50000MB. + + .EXAMPLE + Expand-DbaTLogResponsibly -SqlInstance SqlInstance -Database db1,db2 -TargetLogSizeMB 100 -IncrementSizeMB 10 -ShrinkLogFile -ShrinkSizeMB 10 -BackupDirectory R:\MSSQL\Backup + + Grows the transaction logs for databases db1 and db2 on SQL server SQLInstance to 100MB, sets the incremental growth to 10MB, shrinks the transaction log to 10MB and uses the directory R:\MSSQL\Backup for the required backups. + #> + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')] + param ( + [parameter(Position = 1, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(Position = 3)] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [parameter(Position = 4)] + [object[]]$ExcludeDatabase, + [parameter(Position = 5, Mandatory = $true)] + [int]$TargetLogSizeMB, + [parameter(Position = 6)] + [int]$IncrementSizeMB = -1, + [parameter(Position = 7)] + [int]$LogFileId = -1, + [parameter(Position = 8, ParameterSetName = 'Shrink', Mandatory = $true)] + [switch]$ShrinkLogFile, + [parameter(Position = 9, ParameterSetName = 'Shrink', Mandatory = $true)] + [int]$ShrinkSizeMB, + [parameter(Position = 10, ParameterSetName = 'Shrink')] + [AllowEmptyString()] + [string]$BackupDirectory, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + Write-Message -Level Verbose -Message "Set ErrorActionPreference to Inquire." + $ErrorActionPreference = 'Inquire' + + #Convert MB to KB (SMO works in KB) + Write-Message -Level Verbose -Message "Convert variables MB to KB (SMO works in KB)." + [int]$TargetLogSizeKB = $TargetLogSizeMB * 1024 + [int]$LogIncrementSize = $incrementSizeMB * 1024 + [int]$ShrinkSize = $ShrinkSizeMB * 1024 + [int]$SuggestLogIncrementSize = 0 + [bool]$LogByFileID = if ($LogFileId -eq -1) { + $false + } + else { + $true + } + + #Set base information + Write-Message -Level Verbose -Message "Initialize the instance '$SqlInstance'." + + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + + if ($ShrinkLogFile -eq $true) { + if ($BackupDirectory.length -eq 0) { + $backupdirectory = $server.Settings.BackupDirectory + } + + $pathexists = Test-DbaSqlPath -SqlInstance $server -Path $backupdirectory + + if ($pathexists -eq $false) { + Stop-Function -Message "Backup directory does not exist." + } + } + } + + process { + + try { + + [datetime]$initialTime = Get-Date + + #control the iteration number + $databaseProgressbar = 0; + + Write-Message -Level Verbose -Message "Resolving NetBIOS name." + $sourcenetbios = Resolve-NetBiosName $server + + $databases = $server.Databases | Where-Object IsAccessible + Write-Message -Level Verbose -Message "Number of databases found: $($databases.Count)." + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $database = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + #go through all databases + Write-Message -Level Verbose -Message "Processing...foreach database..." + foreach ($db in $databases.Name) { + Write-Message -Level Verbose -Message "Working on $db." + $databaseProgressbar += 1 + + #set step to reutilize on logging operations + [string]$step = "$databaseProgressbar/$($Database.Count)" + + if ($server.Databases[$db]) { + Write-Progress ` + -Id 1 ` + -Activity "Using database: $db on Instance: '$SqlInstance'" ` + -PercentComplete ($databaseProgressbar / $Database.Count * 100) ` + -Status "Processing - $databaseProgressbar of $($Database.Count)" + + #Validate which file will grow + if ($LogByFileID) { + $logfile = $server.Databases[$db].LogFiles.ItemById($LogFileId) + } + else { + $logfile = $server.Databases[$db].LogFiles[0] + } + + $numLogfiles = $server.Databases[$db].LogFiles.Count + + Write-Message -Level Verbose -Message "$step - Use log file: $logfile." + $currentSize = $logfile.Size + $currentSizeMB = $currentSize / 1024 + + #Get the number of VLFs + $initialVLFCount = Test-DbaDbVirtualLogFile -SqlInstance $server -Database $db + + Write-Message -Level Verbose -Message "$step - Log file current size: $([System.Math]::Round($($currentSize/1024.0), 2)) MB " + [long]$requiredSpace = ($TargetLogSizeKB - $currentSize) + + Write-Message -Level Verbose -Message "Verifying if sufficient space exists ($([System.Math]::Round($($requiredSpace / 1024.0), 2))MB) on the volume to perform this task." + + [long]$TotalTLogFreeDiskSpaceKB = 0 + Write-Message -Level Verbose -Message "Get TLog drive free space" + [object]$AllDrivesFreeDiskSpace = Get-DbaDiskSpace -ComputerName $sourcenetbios | Select-Object Name, SizeInKB + + #Verify path using Split-Path on $logfile.FileName in backwards. This way we will catch the LUNs. Example: "K:\Log01" as LUN name. Need to add final backslash if not there + $DrivePath = Split-Path $logfile.FileName -parent + $DrivePath = if (!($DrivePath.EndsWith("\"))) { "$DrivePath\" } + else { $DrivePath } + Do { + if ($AllDrivesFreeDiskSpace | Where-Object { $DrivePath -eq "$($_.Name)" }) { + $TotalTLogFreeDiskSpaceKB = ($AllDrivesFreeDiskSpace | Where-Object { $DrivePath -eq $_.Name }).SizeInKB + $match = $true + break + } + else { + $match = $false + $DrivePath = Split-Path $DrivePath -parent + $DrivePath = if (!($DrivePath.EndsWith("\"))) { "$DrivePath\" } + else { $DrivePath } + } + + } + while (!$match -or ([string]::IsNullOrEmpty($DrivePath))) + + Write-Message -Level Verbose -Message "Total TLog Free Disk Space in MB: $([System.Math]::Round($($TotalTLogFreeDiskSpaceKB / 1024.0), 2))" + + if (($TotalTLogFreeDiskSpaceKB -le 0) -or ([string]::IsNullOrEmpty($TotalTLogFreeDiskSpaceKB))) { + $title = "Choose increment value for database '$db':" + $message = "Cannot validate freespace on drive where the log file resides. Do you wish to continue? (Y/N)" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + #no + if ($result -eq 1) { + Write-Message -Level Warning -Message "You have cancelled the execution" + return + } + } + + if ($requiredSpace -gt $TotalTLogFreeDiskSpaceKB) { + Write-Message -Level Verbose -Message "There is not enough space on volume to perform this task. `r`n" ` + "Available space: $([System.Math]::Round($($TotalTLogFreeDiskSpaceKB / 1024.0), 2))MB;`r`n" ` + "Required space: $([System.Math]::Round($($requiredSpace / 1024.0), 2))MB;" + return + } + else { + if ($currentSize -ige $TargetLogSizeKB -and ($ShrinkLogFile -eq $false)) { + Write-Message -Level Verbose -Message "$step - [INFO] The T-Log file '$logfile' size is already equal or greater than target size - No action required." + } + else { + Write-Message -Level Verbose -Message "$step - [OK] There is sufficient free space to perform this task." + + # If SQL Server version is greater or equal to 2012 + if ($server.Version.Major -ge "11") { + switch ($TargetLogSizeMB) { + { $_ -le 64 } { $SuggestLogIncrementSize = 64 } + { $_ -ge 64 -and $_ -lt 256 } { $SuggestLogIncrementSize = 256 } + { $_ -ge 256 -and $_ -lt 1024 } { $SuggestLogIncrementSize = 512 } + { $_ -ge 1024 -and $_ -lt 4096 } { $SuggestLogIncrementSize = 1024 } + { $_ -ge 4096 -and $_ -lt 8192 } { $SuggestLogIncrementSize = 2048 } + { $_ -ge 8192 -and $_ -lt 16384 } { $SuggestLogIncrementSize = 4096 } + { $_ -ge 16384 } { $SuggestLogIncrementSize = 8192 } + } + } + # 2008 R2 or under + else { + switch ($TargetLogSizeMB) { + { $_ -le 64 } { $SuggestLogIncrementSize = 64 } + { $_ -ge 64 -and $_ -lt 256 } { $SuggestLogIncrementSize = 256 } + { $_ -ge 256 -and $_ -lt 1024 } { $SuggestLogIncrementSize = 512 } + { $_ -ge 1024 -and $_ -lt 4096 } { $SuggestLogIncrementSize = 1024 } + { $_ -ge 4096 -and $_ -lt 8192 } { $SuggestLogIncrementSize = 2048 } + { $_ -ge 8192 -and $_ -lt 16384 } { $SuggestLogIncrementSize = 4000 } + { $_ -ge 16384 } { $SuggestLogIncrementSize = 8000 } + } + + if (($IncrementSizeMB % 4096) -eq 0) { + Write-Message -Level Verbose -Message "Your instance version is below SQL 2012, remember the known BUG mentioned on HELP. `r`nUse Get-Help Expand-DbaTLogFileResponsibly to read help`r`nUse a different value for incremental size.`r`n" + return + } + } + Write-Message -Level Verbose -Message "Instance $server version: $($server.Version.Major) - Suggested TLog increment size: $($SuggestLogIncrementSize)MB" + + # Shrink Log File to desired size before re-growth to desired size (You need to remove as many VLF's as possible to ensure proper growth) + $ShrinkSizeMB = $ShrinkSize / 1024 + if ($ShrinkLogFile -eq $true) { + if ($server.Databases[$db].RecoveryModel -eq [Microsoft.SqlServer.Management.Smo.RecoveryModel]::Simple) { + Write-Message -Level Warning -Message "Database '$db' is in Simple RecoveryModel which does not allow log backups. Do not specify -ShrinkLogFile and -ShrinkSizeMB parameters." + Continue + } + + try { + $sql = "SELECT last_log_backup_lsn FROM sys.database_recovery_status WHERE database_id = DB_ID('$db')" + $sqlResult = $server.ConnectionContext.ExecuteWithResults($sql); + + if ($sqlResult.Tables[0].Rows[0]["last_log_backup_lsn"] -is [System.DBNull]) { + Write-Message -Level Warning -Message "First, you need to make a full backup before you can do Tlog backup on database '$db' (last_log_backup_lsn is null)." + Continue + } + } + catch { + Stop-Function -Message "Can't execute SQL on $server. `r`n $($_)" -Continue + } + + If ($Pscmdlet.ShouldProcess($($server.name), "Backing up TLog for $db")) { + Write-Message -Level Verbose -Message "We are about to backup the Tlog for database '$db' to '$backupdirectory' and shrink the log." + Write-Message -Level Verbose -Message "Starting Size = $currentSizeMB." + + $DefaultCompression = $server.Configuration.DefaultBackupCompression.ConfigValue + + if ($currentSizeMB -gt $ShrinkSizeMB) { + $backupRetries = 1 + Do { + try { + $percent = $null + $backup = New-Object Microsoft.SqlServer.Management.Smo.Backup + $backup.Action = [Microsoft.SqlServer.Management.Smo.BackupActionType]::Log + $backup.BackupSetDescription = "Transaction Log backup of " + $db + $backup.BackupSetName = $db + " Backup" + $backup.Database = $db + $backup.MediaDescription = "Disk" + $dt = get-date -format yyyyMMddHHmmssms + $null = $backup.Devices.AddDevice($backupdirectory + "\" + $db + "_db_" + $dt + ".trn", 'File') + if ($DefaultCompression = $true) { + $backup.CompressionOption = 1 + } + else { + $backup.CompressionOption = 0 + } + $null = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 2 -ParentId 1 -activity "Backing up $db to $server" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $backup.add_PercentComplete($percent) + $backup.PercentCompleteNotification = 10 + $backup.add_Complete($complete) + Write-Progress -id 2 -ParentId 1 -activity "Backing up $db to $server" -percentcomplete 0 -Status ([System.String]::Format("Progress: {0} %", 0)) + $backup.SqlBackup($server) + Write-Progress -id 2 -ParentId 1 -activity "Backing up $db to $server" -status "Complete" -Completed + $logfile.Shrink($ShrinkSizeMB, [Microsoft.SqlServer.Management.SMO.ShrinkMethod]::TruncateOnly) + $logfile.Refresh() + } + catch { + Write-Progress -id 1 -activity "Backup" -status "Failed" -completed + Stop-Function -Message "Backup failed for database" -ErrorRecord $_ -Target $db -Continue + Continue + } + + } + while (($logfile.Size / 1024) -gt $ShrinkSizeMB -and ++$backupRetries -lt 6) + + $currentSize = $logfile.Size + Write-Message -Level Verbose -Message "TLog backup and truncate for database '$db' finished. Current TLog size after $backupRetries backups is $($currentSize/1024)MB" + } + } + } + + # SMO uses values in KB + $SuggestLogIncrementSize = $SuggestLogIncrementSize * 1024 + + # If default, use $SuggestedLogIncrementSize + if ($IncrementSizeMB -eq -1) { + $LogIncrementSize = $SuggestLogIncrementSize + } + else { + $title = "Choose increment value for database '$db':" + $message = "The input value for increment size was $([System.Math]::Round($LogIncrementSize/1024, 0))MB. However the suggested value for increment is $($SuggestLogIncrementSize/1024)MB.`r`nDo you want to use the suggested value of $([System.Math]::Round($SuggestLogIncrementSize/1024, 0))MB insted of $([System.Math]::Round($LogIncrementSize/1024, 0))MB" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Uses recomended size." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will use parameter value." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + #yes + if ($result -eq 0) { + $LogIncrementSize = $SuggestLogIncrementSize + } + } + + #start growing file + If ($Pscmdlet.ShouldProcess($($server.name), "Starting log growth. Increment chunk size: $($LogIncrementSize/1024)MB for database '$db'")) { + Write-Message -Level Verbose -Message "Starting log growth. Increment chunk size: $($LogIncrementSize/1024)MB for database '$db'" + + Write-Message -Level Verbose -Message "$step - While current size less than target log size." + + while ($currentSize -lt $TargetLogSizeKB) { + + Write-Progress ` + -Id 2 ` + -ParentId 1 ` + -Activity "Growing file $logfile on '$db' database" ` + -PercentComplete ($currentSize / $TargetLogSizeKB * 100) ` + -Status "Remaining - $([System.Math]::Round($($($TargetLogSizeKB - $currentSize) / 1024.0), 2)) MB" + + Write-Message -Level Verbose -Message "$step - Verifying if the log can grow or if it's already at the desired size." + if (($TargetLogSizeKB - $currentSize) -lt $LogIncrementSize) { + Write-Message -Level Verbose -Message "$step - Log size is lower than the increment size. Setting current size equals $TargetLogSizeKB." + $currentSize = $TargetLogSizeKB + } + else { + Write-Message -Level Verbose -Message "$step - Grow the $logfile file in $([System.Math]::Round($($LogIncrementSize / 1024.0), 2)) MB" + $currentSize += $LogIncrementSize + } + + #When -WhatIf Switch, do not run + if ($PSCmdlet.ShouldProcess("$step - File will grow to $([System.Math]::Round($($currentSize/1024.0), 2)) MB", "This action will grow the file $logfile on database $db to $([System.Math]::Round($($currentSize/1024.0), 2)) MB .`r`nDo you wish to continue?", "Perform grow")) { + Write-Message -Level Verbose -Message "$step - Set size $logfile to $([System.Math]::Round($($currentSize/1024.0), 2)) MB" + $logfile.size = $currentSize + + Write-Message -Level Verbose -Message "$step - Applying changes" + $logfile.Alter() + Write-Message -Level Verbose -Message "$step - Changes have been applied" + + #Will put the info like VolumeFreeSpace up to date + $logfile.Refresh() + } + } + + Write-Message -Level Verbose -Message "`r`n$step - [OK] Growth process for logfile '$logfile' on database '$db', has been finished." + + Write-Message -Level Verbose -Message "$step - Grow $logfile log file on $db database finished." + } + } + } #else space available + } + #else verifying existence + else { + Write-Message -Level Verbose -Message "Database '$db' does not exist on instance '$SqlInstance'." + } + + #Get the number of VLFs + $currentVLFCount = Test-DbaDbVirtualLogFile -SqlInstance $server -Database $db + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + ID = $logfile.ID + Name = $logfile.Name + LogFileCount = $numLogfiles + InitialSize = [dbasize]($currentSizeMB * 1024) + CurrentSize = [dbasize]($TargetLogSizeMB * 1024) + InitialVLFCount = $initialVLFCount.Total + CurrentVLFCount = $currentVLFCount.Total + } | Select-DefaultView -ExcludeProperty LogFileCount + } #foreach database + } + catch { + Stop-Function -Message "Logfile $logfile on database $db not processed. Error: $($_.Exception.Message). Line Number: $($_InvocationInfo.ScriptLineNumber)" -Continue + } + } + + end { + Write-Message -Level Verbose -Message "Process finished $((Get-Date) - ($initialTime))" + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Expand-SqlTLogResponsibly + } +} +#ValidationTags#Messaging# +function Export-DbaAvailabilityGroup { + <# + .SYNOPSIS + Exports SQL Server Availability Groups to a T-SQL file. + + .DESCRIPTION + Exports SQL Server Availability Groups creation scripts to a T-SQL file. This is a function that is not available in SSMS. + + .PARAMETER SqlInstance + The SQL Server instance name. SQL Server 2012 and above supported. + + .PARAMETER FilePath + The directory name where the output files will be written. A sub directory with the format 'ServerName$InstanceName' will be created. A T-SQL scripts named 'AGName.sql' will be created under this subdirectory for each scripted Availability Group. + + .PARAMETER AvailabilityGroup + The Availability Group(s) to export - this list is auto-populated from the server. If unspecified, all logins will be processed. + + .PARAMETER ExcludeAvailabilityGroup + The Availability Group(s) to exclude - this list is auto-populated from the server. + + .PARAMETER NoClobber + Do not overwrite existing export files. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + Shows you what it'd output if you were to run the command + + .PARAMETER Confirm + Confirms each step/line of output + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Chris Sommer (@cjsommer), cjsommer.com + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaAvailabilityGroup + + .EXAMPLE + Export-DbaAvailabilityGroup -SqlInstance sql2012 + + Exports all Availability Groups from SQL server "sql2012". Output scripts are written to the Documents\SqlAgExports directory by default. + + .EXAMPLE + Export-DbaAvailabilityGroup -SqlInstance sql2012 -FilePath C:\temp\availability_group_exports + + Exports all Availability Groups from SQL server "sql2012". Output scripts are written to the C:\temp\availability_group_exports directory. + + .EXAMPLE + Export-DbaAvailabilityGroup -SqlInstance sql2012 -FilePath 'C:\dir with spaces\availability_group_exports' -AvailabilityGroups AG1,AG2 + + Exports Availability Groups AG1 and AG2 from SQL server "sql2012". Output scripts are written to the C:\dir with spaces\availability_group_exports directory. + + .EXAMPLE + Export-DbaAvailabilityGroup -SqlInstance sql2014 -FilePath C:\temp\availability_group_exports -NoClobber + + Exports all Availability Groups from SQL server "sql2014". Output scripts are written to the C:\temp\availability_group_exports directory. If the export file already exists it will not be overwritten. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$AvailabilityGroup, + [object[]]$ExcludeAvailabilityGroup, + [Alias("OutputLocation", "Path")] + [string]$FilePath = "$([Environment]::GetFolderPath("MyDocuments"))\SqlAgExport", + [switch]$NoClobber, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.IsHadrEnabled -eq $false) { + Stop-Function -Message "Hadr is not enabled on this instance" -Continue + } + else { + # Get all of the Availability Groups and filter if required + $ags = $server.AvailabilityGroups + } + + if (Test-Bound 'AvailabilityGroup') { + $ags = $ags | Where-Object Name -In $AvailabilityGroup + } + if (Test-Bound 'ExcludeAvailabilityGroup') { + $ags = $ags | Where-Object Name -NotIn $ExcludeAvailabilityGroup + } + + if ($ags) { + + # Set and create the OutputLocation if it doesn't exist + $sqlinst = $instance.ToString().Replace('\', '$') + $outputLocation = "$FilePath\$sqlinst" + + if (!(Test-Path $outputLocation -PathType Container)) { + $null = New-Item -Path $outputLocation -ItemType Directory -Force + } + + # Script each Availability Group + foreach ($ag in $ags) { + $agName = $ag.Name + + # Set the outfile name + if ($AppendDateToOutputFilename.IsPresent) { + $formatteddate = (Get-Date -Format 'yyyyMMdd_hhmm') + $outFile = "$outputLocation\${AGname}_${formatteddate}.sql" + } + else { + $outFile = "$outputLocation\$agName.sql" + } + + # Check NoClobber and script out the AG + if ($NoClobber.IsPresent -and (Test-Path -Path $outFile -PathType Leaf)) { + Write-Message -Level Warning -Message "OutputFile $outFile already exists. Skipping due to -NoClobber parameter" + } + else { + Write-Message -Level Verbose -Message "Scripting Availability Group [$agName] on $instance to $outFile" + + # Create comment block header for AG script + "/*" | Out-File -FilePath $outFile -Encoding ASCII -Force + " * Created by dbatools 'Export-DbaAvailabilityGroup' cmdlet on '$(Get-Date)'" | Out-File -FilePath $outFile -Encoding ASCII -Append + " * See https://dbatools.io/Export-DbaAvailabilityGroup for more help" | Out-File -FilePath $outFile -Encoding ASCII -Append + + # Output AG and listener names + " *" | Out-File -FilePath $outFile -Encoding ASCII -Append + " * Availability Group Name: $($ag.name)" | Out-File -FilePath $outFile -Encoding ASCII -Append + $ag.AvailabilityGroupListeners | ForEach-Object { " * Listener Name: $($_.name)" } | Out-File -FilePath $outFile -Encoding ASCII -Append + + # Output all replicas + " *" | Out-File -FilePath $outFile -Encoding ASCII -Append + $ag.AvailabilityReplicas | ForEach-Object { " * Replica: $($_.name)" } | Out-File -FilePath $outFile -Encoding ASCII -Append + + # Output all databases + " *" | Out-File -FilePath $outFile -Encoding ASCII -Append + $ag.AvailabilityDatabases | ForEach-Object { " * Database: $($_.name)" } | Out-File -FilePath $outFile -Encoding ASCII -Append + + # $ag | Select-Object -Property * | Out-File -FilePath $outFile -Encoding ASCII -Append + + "*/" | Out-File -FilePath $outFile -Encoding ASCII -Append + + # Script the AG + try { + $ag.Script() | Out-File -FilePath $outFile -Encoding ASCII -Append + Get-ChildItem $outFile + } + catch { + Stop-Function -ErrorRecord $_ -Message "Error scripting out the availability groups. This is likely due to a bug in SMO." -Continue + } + } + } + } + else { + Write-Message -Level Output -Message "No Availability Groups detected on $instance" + } + } + } +} +function Export-DbaDacpac { + <# + .SYNOPSIS + Exports a dacpac from a server. + + .DESCRIPTION + Using SQLPackage, export a dacpac from an instance of SQL Server. + + Note - Extract from SQL Server is notoriously flaky - for example if you have three part references to external databases it will not work. + + For help with the extract action parameters and properties, refer to https://msdn.microsoft.com/en-us/library/hh550080(v=vs.103).aspx + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to and publish to. + + .PARAMETER SqlCredential + Allows you to login to servers using alternative logins instead Integrated, accepts Credential object created by Get-Credential + + .PARAMETER Path + The directory where the .dacpac files will be exported to. Defaults to documents. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllUserDatabases + Run command against all user databases + + .PARAMETER ExtendedParameters + Optional parameters used to extract the DACPAC. More information can be found at + https://msdn.microsoft.com/en-us/library/hh550080.aspx + + .PARAMETER ExtendedProperties + Optional properties used to extract the DACPAC. More information can be found at + https://msdn.microsoft.com/en-us/library/hh550080.aspx + + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Database, Dacpac + Author: Richie lee (@bzzzt_io) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaDacpac + + .EXAMPLE + Export-DbaDacpac -SqlInstance sql2016 -Database SharePoint_Config + Exports the dacpac for SharePoint_Config on sql2016 to $home\Documents\SharePoint_Config.dacpac + + .EXAMPLE + $moreprops = "/p:VerifyExtraction=$true /p:CommandTimeOut=10" + Export-DbaDacpac -SqlInstance sql2016 -Database SharePoint_Config -Path C:\temp -ExtendedProperties $moreprops + + Sets the CommandTimeout to 10 then extracts the dacpac for SharePoint_Config on sql2016 to C:\temp\SharePoint_Config.dacpac then verifies extraction. + + + #> + [CmdletBinding()] + param + ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllUserDatabases, + [string]$Path = "$home\Documents", + [string]$ExtendedParameters, + [string]$ExtendedProperties, + [switch]$EnableException + ) + + process { + if ((Test-Bound -Not -ParameterName Database) -and (Test-Bound -Not -ParameterName ExcludeDatabase) -and (Test-Bound -Not -ParameterName AllUserDatabases)) { + Stop-Function -Message "You must specify databases to execute against using either -Database, -ExcludeDatabase or -AllUserDatabases" + } + + if (-not (Test-Path $Path)) { + Stop-Function -Message "$Path doesn't exist or access denied" + } + + if ((Get-Item $path) -isnot [System.IO.DirectoryInfo]) { + Stop-Function -Message "Path must be a directory" + } + + foreach ($instance in $sqlinstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $cleaninstance = $instance.ToString().Replace('\', '-') + + $dbs = $server.Databases | Where-Object { $_.IsSystemObject -eq $false -and $_.IsAccessible } + + if ($Database) { + $dbs = $dbs | Where-Object Name -in $Database + if (-not $dbs.name) { + Stop-Function -Message "Database $Database does not exist on $instance" -Target $instance -Continue + } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -notin $ExcludeDatabase + } + + foreach ($db in $dbs) { + $dbname = $db.name + $connstring = $server.ConnectionContext.ConnectionString.Replace('"', "'") + if ($connstring -notmatch 'Database=') { + $connstring = "$connstring;Database=$dbname" + } + $filename = "$Path\$cleaninstance-$dbname.dacpac" + Write-Message -Level Verbose -Message "Exporting $filename" + Write-Message -Level Verbose -Message "Using connection string $connstring" + + $sqlPackageArgs = "/action:Extract /tf:""$filename"" /SourceConnectionString:""$connstring"" $ExtendedParameters $ExtendedProperties" + $resultstime = [diagnostics.stopwatch]::StartNew() + + try { + $startprocess = New-Object System.Diagnostics.ProcessStartInfo + $startprocess.FileName = "$script:PSModuleRoot\bin\smo\sqlpackage.exe" + $startprocess.Arguments = $sqlPackageArgs + $startprocess.RedirectStandardError = $true + $startprocess.RedirectStandardOutput = $true + $startprocess.UseShellExecute = $false + $startprocess.CreateNoWindow = $true + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startprocess + $process.Start() | Out-Null + $process.WaitForExit() + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + Write-Message -level Verbose -Message "StandardOutput: $stdout" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $dbname + Path = $filename + Elapsed = [prettytimespan]($resultstime.Elapsed) + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName + } + catch { + Stop-Function -Message "SQLPackage Failure" -ErrorRecord $_ -Continue + } + + if ($process.ExitCode -ne 0) { + Stop-Function -Message "Standard output - $stderr" -Continue + } + } + } + } +} +function Export-DbaDiagnosticQuery { + <# + .SYNOPSIS + Export-DbaDiagnosticQuery can convert ouput generated by Invoke-DbaDiagnosticQuery to CSV or Excel + + .DESCRIPTION + The default output format of Invoke-DbaDiagnosticQuery is a custom object. It can also output to CSV and Excel. + However, CSV output can generate a lot of files and Excel output depends on the ImportExcel module by Doug Fike (https://github.com/dfinke/ImportExcel) + Export-DbaDiagnosticQuery can be used to convert from the default export type to the other available export types. + + .PARAMETER InputObject + Specifies the objects to convert + + .PARAMETER ConvertTo + Specifies the output type. Valid choices are Excel and CSV. CSV is the default. + + .PARAMETER Path + Specifies the path to the output files. + + .PARAMETER Suffix + Suffix for the filename. It's datetime by default. + + .PARAMETER NoPlanExport + Use this switch to suppress exporting of .sqlplan files + + .PARAMETER NoQueryExport + Use this switch to suppress exporting of .sql files + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Query + Author: Andre Kamman (@AndreKamman), http://clouddba.io + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaDiagnosticQuery + + .EXAMPLE + Invoke-DbaDiagnosticQuery -SqlInstance sql2016 | Export-DbaDiagnosticQuery -Path c:\temp + + Converts output from Invoke-DbaDiagnosticQuery to multiple CSV files + + .EXAMPLE + $output = Invoke-DbaDiagnosticQuery -SqlInstance sql2016 + Export-DbaDiagnosticQuery -InputObject $output -ConvertTo Excel + + Converts output from Invoke-DbaDiagnosticQuery to Excel worksheet(s) in the Documents folder + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$InputObject, + [ValidateSet("Excel", "Csv")] + [string]$ConvertTo = "Csv", + [System.IO.FileInfo]$Path = [Environment]::GetFolderPath("mydocuments"), + [string]$Suffix = "$(Get-Date -format 'yyyyMMddHHmmssms')", + [switch]$NoPlanExport, + [switch]$NoQueryExport, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($ConvertTo -eq "Excel") { + try { + Import-Module ImportExcel -ErrorAction Stop + } + catch { + $message = "Failed to load module, exporting to Excel feature is not available + Install the module from: https://github.com/dfinke/ImportExcel + Valid alternative conversion format is csv" + Stop-Function -Message $message + return + } + } + + if (!$(Test-Path $Path)) { + try { + New-Item $Path -ItemType Directory -ErrorAction Stop | Out-Null + Write-Message -Level Output -Message "Created directory $Path" + } + catch { + Stop-Function -Message "Failed to create directory $Path" -Continue + } + } + + Function Remove-InvalidFileNameChars { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [String]$Name + ) + $Name = $Name.Replace(" ", "-") + $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join '' + $re = "[{0}]" -f [RegEx]::Escape($invalidChars) + return ($Name -replace $re) + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($row in $InputObject) { + $result = $row.Result + $name = $row.Name + $SqlInstance = $row.SqlInstance.Replace("\", "$") + $dbname = $row.DatabaseName + $number = $row.Number + + if ($null -eq $result) { + Stop-Function -Message "Result was empty for $name" -Target $result -Continue + } + + $queryname = Remove-InvalidFileNameChars -Name $Name + $excelfilename = "$Path\$SqlInstance-DQ-$Suffix.xlsx" + $exceldbfilename = "$Path\$SqlInstance-DQ-$dbname-$Suffix.xlsx" + $csvdbfilename = "$Path\$SqlInstance-$dbname-DQ-$number-$queryname-$Suffix.csv" + $csvfilename = "$Path\$SqlInstance-DQ-$number-$queryname-$Suffix.csv" + + $columnnameoptions = "Query Plan", "QueryPlan", "Query_Plan", "query_plan_xml" + if (($result | Get-Member | Where-Object Name -in $columnnameoptions).Count -gt 0) { + $plannr = 0 + $columnname = ($result | Get-Member | Where-Object Name -In $columnnameoptions).Name + foreach ($plan in $result."$columnname") { + $plannr += 1 + if ($row.DatabaseSpecific) { + $planfilename = "$Path\$SqlInstance-$dbname-DQ-$number-$queryname-$plannr-$Suffix.sqlplan" + } + else { + $planfilename = "$Path\$SqlInstance-DQ-$number-$queryname-$plannr-$Suffix.sqlplan" + } + + if (!$NoPlanExport) { + Write-Message -Level Output -Message "Exporting $planfilename" + if ($plan) {$plan | Out-File -FilePath $planfilename} + } + } + + $result = $result | Select-Object * -ExcludeProperty "$columnname" + } + + $columnnameoptions = "Complete Query Text", "QueryText", "Query Text", "Query_Text", "query_sql_text" + if (($result | Get-Member | Where-Object Name -In $columnnameoptions ).Count -gt 0) { + $sqlnr = 0 + $columnname = ($result | Get-Member | Where-Object Name -In $columnnameoptions).Name + foreach ($sql in $result."$columnname") { + $sqlnr += 1 + if ($row.DatabaseSpecific) { + $sqlfilename = "$Path\$SqlInstance-$dbname-DQ-$number-$queryname-$sqlnr-$Suffix.sql" + } + else { + $sqlfilename = "$Path\$SqlInstance-DQ-$number-$queryname-$sqlnr-$Suffix.sql" + } + + if (!$NoQueryExport) { + Write-Message -Level Output -Message "Exporting $sqlfilename" + if ($sql) {$sql | Out-File -FilePath $sqlfilename} + } + } + + $result = $result | Select-Object * -ExcludeProperty "$columnname" + } + + switch ($ConvertTo) { + "Excel" { + if ($row.DatabaseSpecific) { + Write-Message -Level Output -Message "Exporting $exceldbfilename" + $result | Export-Excel -Path $exceldbfilename -WorkSheetname $Name -AutoSize -AutoFilter -BoldTopRow -FreezeTopRow + } + else { + Write-Message -Level Output -Message "Exporting $excelfilename" + $result | Export-Excel -Path $excelfilename -WorkSheetname $Name -AutoSize -AutoFilter -BoldTopRow -FreezeTopRow + } + } + "csv" { + if ($row.DatabaseSpecific) { + Write-Message -Level Output -Message "Exporting $csvdbfilename" + $result | Export-Csv -Path $csvdbfilename -NoTypeInformation -Append + } + else { + Write-Message -Level Output -Message "Exporting $csvfilename" + $result | Export-Csv -Path $csvfilename -NoTypeInformation -Append + } + } + } + } + } +} +#ValidationTags#Messaging# +function Export-DbaExecutionPlan { + <# + .SYNOPSIS + Exports execution plans to disk. + + .DESCRIPTION + Exports execution plans to disk. Can pipe from Export-DbaExecutionPlan + + Thanks to + https://www.simple-talk.com/sql/t-sql-programming/dmvs-for-query-plan-metadata/ + and + http://www.scarydba.com/2017/02/13/export-plans-cache-sqlplan-file/ + for the idea and query. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER SinceCreation + Datetime object used to narrow the results to a date + + .PARAMETER SinceLastExecution + Datetime object used to narrow the results to a date + + .PARAMETER Path + The directory where all of the sqlxml files will be exported + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER PipedObject + Internal parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, ExecutionPlan + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaExecutionPlan + + .EXAMPLE + Export-DbaExecutionPlan -SqlInstance sqlserver2014a + + Exports all execution plans for sqlserver2014a. + + .EXAMPLE + Export-DbaExecutionPlan -SqlInstance sqlserver2014a -Database db1, db2 -SinceLastExecution '7/1/2016 10:47:00' + + Exports all execution plans for databases db1 and db2 on sqlserver2014a since July 1, 2016 at 10:47 AM. + #> + [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param ( + [parameter(ParameterSetName = 'NotPiped', Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(ParameterSetName = 'NotPiped')] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(ParameterSetName = 'Piped', Mandatory)] + [parameter(ParameterSetName = 'NotPiped', Mandatory)] + [string]$Path, + [parameter(ParameterSetName = 'NotPiped')] + [datetime]$SinceCreation, + [parameter(ParameterSetName = 'NotPiped')] + [datetime]$SinceLastExecution, + [Parameter(ParameterSetName = 'Piped', Mandatory, ValueFromPipeline)] + [object[]]$PipedObject, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($SinceCreation -ne $null) { + $SinceCreation = $SinceCreation.ToString("yyyy-MM-dd HH:mm:ss") + } + + if ($SinceLastExecution -ne $null) { + $SinceLastExecution = $SinceLastExecution.ToString("yyyy-MM-dd HH:mm:ss") + } + + function Export-Plan { + param( + [object]$object + ) + $instanceName = $object.SqlInstance + $dbName = $object.DatabaseName + $queryPosition = $object.QueryPosition + $sqlHandle = "0x"; $object.SqlHandle | ForEach-Object { $sqlHandle += ("{0:X}" -f $_).PadLeft(2, "0") } + $sqlHandle = $sqlHandle.TrimStart('0x02000000').TrimEnd('0000000000000000000000000000000000000000') + $shortName = "$instanceName-$dbName-$queryPosition-$sqlHandle" + + foreach ($queryPlan in $object.BatchQueryPlanRaw) { + $fileName = "$path\$shortName-batch.sqlplan" + + try { + if ($Pscmdlet.ShouldProcess("localhost", "Writing XML file to $fileName")) { + $queryPlan.Save($fileName) + } + } + catch { + Stop-Function -Message "Skipped query plan for $fileName because it is null." -Target $fileName -ErrorRecord $_ -Continue + } + } + + foreach ($statementPlan in $object.SingleStatementPlanRaw) { + $fileName = "$path\$shortName.sqlplan" + + try { + if ($Pscmdlet.ShouldProcess("localhost", "Writing XML file to $fileName")) { + $statementPlan.Save($fileName) + } + } + catch { + Stop-Function -Message "Skipped statement plan for $fileName because it is null." -Target $fileName -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess("console", "Showing output object")) { + Add-Member -Force -InputObject $object -MemberType NoteProperty -Name OutputFile -Value $fileName + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, DatabaseName, SqlHandle, CreationTime, LastExecutionTime, OutputFile + } + } + } + + process { + if (!(Test-Path $Path)) { + $null = New-Item -ItemType Directory -Path $Path + } + + if ($PipedObject) { + foreach ($object in $pipedobject) { + Export-Plan $object + return + } + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $select = "SELECT DB_NAME(deqp.dbid) as DatabaseName, OBJECT_NAME(deqp.objectid) as ObjectName, + detqp.query_plan AS SingleStatementPlan, + deqp.query_plan AS BatchQueryPlan, + ROW_NUMBER() OVER ( ORDER BY Statement_Start_offset ) AS QueryPosition, + sql_handle as SqlHandle, + plan_handle as PlanHandle, + creation_time as CreationTime, + last_execution_time as LastExecutionTime" + + $from = " FROM sys.dm_exec_query_stats deqs + CROSS APPLY sys.dm_exec_text_query_plan(deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset) AS detqp + CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) AS deqp + CROSS APPLY sys.dm_exec_sql_text(deqs.plan_handle) AS execText" + + if ($ExcludeDatabase -or $Database -or $SinceCreation.Length -gt 0 -or $SinceLastExecution.length -gt 0 -or $ExcludeEmptyQueryPlan -eq $true) { + $where = " WHERE " + } + + $whereArray = @() + + if ($Database -gt 0) { + $dbList = $Database -join "','" + $whereArray += " DB_NAME(deqp.dbid) in ('$dbList') " + } + + if (Test-Bound 'SinceCreation') { + Write-Message -Level Verbose -Message "Adding creation time" + $whereArray += " creation_time >= '$SinceCreation' " + } + + if (Test-Bound 'SinceLastExecution') { + Write-Message -Level Verbose -Message "Adding last execution time" + $whereArray += " last_execution_time >= '$SinceLastExecution' " + } + + if (Test-Bound 'ExcludeDatabase') { + $dbList = $ExcludeDatabase -join "','" + $whereArray += " DB_NAME(deqp.dbid) not in ('$dbList') " + } + + if (Test-Bound 'ExcludeEmptyQueryPlan') { + $whereArray += " detqp.query_plan is not null" + } + + if ($where.Length -gt 0) { + $whereArray = $whereArray -join " and " + $where = "$where $whereArray" + } + + $sql = "$select $from $where" + Write-Message -Level Debug -Message "SQL Statement: $sql" + try { + $dataTable = $server.ConnectionContext.ExecuteWithResults($sql).Tables + } + catch { + Stop-Function -Message "Issue collecting execution plans" -Target $instance -ErroRecord $_ -Continue + } + + foreach ($row in ($dataTable.Rows)) { + $sqlHandle = "0x"; $row.sqlhandle | ForEach-Object { $sqlHandle += ("{0:X}" -f $_).PadLeft(2, "0") } + $planhandle = "0x"; $row.planhandle | ForEach-Object { $planhandle += ("{0:X}" -f $_).PadLeft(2, "0") } + + $object = [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $row.DatabaseName + SqlHandle = $sqlHandle + PlanHandle = $planhandle + SingleStatementPlan = $row.SingleStatementPlan + BatchQueryPlan = $row.BatchQueryPlan + QueryPosition = $row.QueryPosition + CreationTime = $row.CreationTime + LastExecutionTime = $row.LastExecutionTime + BatchQueryPlanRaw = [xml]$row.BatchQueryPlan + SingleStatementPlanRaw = [xml]$row.SingleStatementPlan + } + Export-Plan $object + } + } + } +} +function Export-DbaLogin { + <# + .SYNOPSIS + Exports Windows and SQL Logins to a T-SQL file. Export includes login, SID, password, default database, default language, server permissions, server roles, db permissions, db roles. + + .DESCRIPTION + Exports Windows and SQL Logins to a T-SQL file. Export includes login, SID, password, default database, default language, server permissions, server roles, db permissions, db roles. + + .PARAMETER SqlInstance + The SQL Server instance name. SQL Server 2000 and above supported. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Login + The login(s) to process. Options for this list are auto-populated from the server. If unspecified, all logins will be processed. + + .PARAMETER ExcludeLogin + The login(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER FilePath + The file to write to. + + .PARAMETER NoClobber + If this switch is enabled, a file already existing at the path specified by FilePath will not be overwritten. + + .PARAMETER Append + If this switch is enabled, content will be appended to a file already existing at the path specified by FilePath. If the file does not exist, it will be created. + + .PARAMETER NoJobs + If this switch is enabled, Agent job ownership will not be exported. + + .PARAMETER NoDatabases + If this switch is enabled, mappings for databases will not be exported. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER ExcludeGoBatchSeparator + If specified, will NOT script the 'GO' batch separator. + + .PARAMETER DestinationVersion + To say to which version the script should be generated. If not specified will use instance major version. + + .NOTES + Tags: Export, Login + Author: Chrissy LeMaire (@cl), netnerds.net + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaLogin + + .EXAMPLE + Export-DbaLogin -SqlInstance sql2005 -FilePath C:\temp\sql2005-logins.sql + + Exports the logins for SQL Server "sql2005" and writes them to the file "C:\temp\sql2005-logins.sql" + + .EXAMPLE + Export-DbaLogin -SqlInstance sqlserver2014a -Exclude realcajun -SqlCredential $scred -FilePath C:\temp\logins.sql -Append + + Authenticates to sqlserver2014a using SQL Authentication. Exports all logins except for realcajun to C:\temp\logins.sql, and appends to the file if it exists. If not, the file will be created. + + .EXAMPLE + Export-DbaLogin -SqlInstance sqlserver2014a -Login realcajun, netnerds -FilePath C:\temp\logins.sql + + Exports ONLY logins netnerds and realcajun FROM sqlserver2014a to the file C:\temp\logins.sql + + .EXAMPLE + Export-DbaLogin -SqlInstance sqlserver2014a -Login realcajun, netnerds -Database HR, Accounting + + Exports ONLY logins netnerds and realcajun FROM sqlserver2014a with the permissions on databases HR and Accounting + + .EXAMPLE + Export-DbaLogin -SqlInstance sqlserver2008 -Login realcajun, netnerds -FilePath C:\temp\login.sql -ExcludeGoBatchSeparator + + Exports ONLY logins netnerds and realcajun FROM sqlserver2008 server, to the C:\temp\login.sql file without the 'GO' batch separator. + + .EXAMPLE + Export-DbaLogin -SqlInstance sqlserver2008 -Login realcajun -FilePath C:\temp\users.sql -DestinationVersion SQLServer2016 + + Exports login realcajun fron sqlsever2008 to the file C:\temp\users.sql with sintax to run on SQL Server 2016 + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [object[]]$Login, + [object[]]$ExcludeLogin, + [Alias("Databases")] + [object[]]$Database, + [Alias("OutFile", "Path", "FileName")] + [string]$FilePath, + [Alias("NoOverwrite")] + [switch]$NoClobber, + [switch]$Append, + [switch]$NoDatabases, + [switch]$NoJobs, + [Alias('Silent')] + [switch]$EnableException, + [switch]$ExcludeGoBatchSeparator, + [ValidateSet('SQLServer2000', 'SQLServer2005', 'SQLServer2008/2008R2', 'SQLServer2012', 'SQLServer2014', 'SQLServer2016', 'SQLServer2017')] + [string]$DestinationVersion + ) + + begin { + + if ($FilePath) { + if ($FilePath -notlike "*\*") { + $FilePath = ".\$filepath" + } + $directory = Split-Path $FilePath + $exists = Test-Path $directory + + if ($exists -eq $false) { + Write-Message -Level Warning -Message "Parent directory $directory does not exist." + } + } + + $outsql = @() + + $versions = @{ + 'SQLServer2000' = 'Version80' + 'SQLServer2005' = 'Version90' + 'SQLServer2008/2008R2' = 'Version100' + 'SQLServer2012' = 'Version110' + 'SQLServer2014' = 'Version120' + 'SQLServer2016' = 'Version130' + 'SQLServer2017' = 'Version140' + } + + $versionsNumbers = @{ + '8' = 'Version80' + '9' = 'Version90' + '10' = 'Version100' + '11' = 'Version110' + '12' = 'Version120' + '13' = 'Version130' + '14' = 'Version140' + } + } + process { + if (Test-FunctionInterrupt) { + return + } + + Write-Message -Level Verbose -Message "Connecting to $sqlinstance." + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlcredential + + if ([string]::IsNullOrEmpty($destinationVersion)) { + #Get compatibility level for scripting the objects + $scriptVersion = $versionsNumbers[$server.VersionMajor.ToString()] + } + else { + $scriptVersion = $versions[$destinationVersion] + } + + if ($NoDatabases -eq $false -or $Database) { + # if we got a database or a list of databases passed + # and we need to enumerate mappings, login.enumdatabasemappings() takes forever + # the cool thing though is that database.enumloginmappings() is fast. A lot. + # if we get a list of databases passed (or even the default list of all the databases) + # we save outself a call to enumloginmappings if there is no map at all + $DbMapping = @() + $DbsToMap = $server.Databases + if ($Database) { + $DbsToMap = $DbsToMap | Where-Object Name -in $Database + } + foreach ($db in $DbsToMap) { + if ($db.IsAccessible -eq $false) { + continue + } + $dbmap = $db.EnumLoginMappings() + foreach ($el in $dbmap) { + $DbMapping += [pscustomobject]@{ + Database = $db.Name + UserName = $el.Username + LoginName = $el.LoginName + } + } + } + } + + foreach ($sourceLogin in $server.Logins) { + $userName = $sourceLogin.name + + if ($Login -and $Login -notcontains $userName -or $ExcludeLogin -contains $userName) { + continue + } + + if ($userName.StartsWith("##") -or $userName -eq 'sa') { + Write-Message -Level Warning -Message "Skipping $userName." + continue + } + + $serverName = $server + + $userBase = ($userName.Split("\")[0]).ToLower() + if ($serverName -eq $userBase -or $userName.StartsWith("NT ")) { + if ($Pscmdlet.ShouldProcess("console", "Stating $userName is skipped because it is a local machine name.")) { + Write-Message -Level Warning -Message "$userName is skipped because it is a local machine name." + continue + } + } + + if ($Pscmdlet.ShouldProcess("Outfile", "Adding T-SQL for login $userName")) { + if ($FilePath) { + Write-Message -Level Output -Message "Exporting $userName." + } + + $outsql += "`r`nUSE master`n" + # Getting some attributes + $defaultDb = $sourceLogin.DefaultDatabase + $language = $sourceLogin.Language + + if ($sourceLogin.PasswordPolicyEnforced -eq $false) { + $checkPolicy = "OFF" + } + else { + $checkPolicy = "ON" + } + + if (!$sourceLogin.PasswordExpirationEnabled) { + $checkExpiration = "OFF" + } + else { + $checkExpiration = "ON" + } + + # Attempt to script out SQL Login + if ($sourceLogin.LoginType -eq "SqlLogin") { + $sourceLoginName = $sourceLogin.name + + switch ($server.versionMajor) { + 0 { + $sql = "SELECT CONVERT(VARBINARY(256),password) AS hashedpass FROM master.dbo.syslogins WHERE loginname='$sourceLoginName'" + } + 8 { + $sql = "SELECT CONVERT(VARBINARY(256),password) AS hashedpass FROM dbo.syslogins WHERE name='$sourceLoginName'" + } + 9 { + $sql = "SELECT CONVERT(VARBINARY(256),password_hash) as hashedpass FROM sys.sql_logins WHERE name='$sourceLoginName'" + } + default { + $sql = "SELECT CAST(CONVERT(varchar(256), CAST(LOGINPROPERTY(name,'PasswordHash') AS VARBINARY(256)), 1) AS NVARCHAR(max)) AS hashedpass FROM sys.server_principals WHERE principal_id = $($sourceLogin.id)" + } + } + + try { + $hashedPass = $server.ConnectionContext.ExecuteScalar($sql) + } + catch { + $hashedPassDt = $server.Databases['master'].ExecuteWithResults($sql) + $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) + } + + if ($hashedPass.GetType().Name -ne "String") { + $passString = "0x"; $hashedPass | ForEach-Object { + $passString += ("{0:X}" -f $_).PadLeft(2, "0") + } + $hashedPass = $passString + } + + $sid = "0x"; $sourceLogin.sid | ForEach-Object { + $sid += ("{0:X}" -f $_).PadLeft(2, "0") + } + $outsql += "IF NOT EXISTS (SELECT loginname FROM master.dbo.syslogins WHERE name = '$userName') CREATE LOGIN [$userName] WITH PASSWORD = $hashedPass HASHED, SID = $sid, DEFAULT_DATABASE = [$defaultDb], CHECK_POLICY = $checkPolicy, CHECK_EXPIRATION = $checkExpiration, DEFAULT_LANGUAGE = [$language]" + } + # Attempt to script out Windows User + elseif ($sourceLogin.LoginType -eq "WindowsUser" -or $sourceLogin.LoginType -eq "WindowsGroup") { + $outsql += "IF NOT EXISTS (SELECT loginname FROM master.dbo.syslogins WHERE name = '$userName') CREATE LOGIN [$userName] FROM WINDOWS WITH DEFAULT_DATABASE = [$defaultDb], DEFAULT_LANGUAGE = [$language]" + } + # This script does not currently support certificate mapped or asymmetric key users. + else { + Write-Message -Level Warning -Message "$($sourceLogin.LoginType) logins not supported. $($sourceLogin.Name) skipped." + continue + } + + if ($sourceLogin.IsDisabled) { + $outsql += "ALTER LOGIN [$userName] DISABLE" + } + + if ($sourceLogin.DenyWindowsLogin) { + $outsql += "DENY CONNECT SQL TO [$userName]" + } + } + + # Server Roles: sysadmin, bulklogin, etc + foreach ($role in $server.Roles) { + $roleName = $role.Name + + # SMO changed over time + try { + $roleMembers = $role.EnumMemberNames() + } + catch { + $roleMembers = $role.EnumServerRoleMembers() + } + + if ($roleMembers -contains $userName) { + if (($server.VersionMajor -lt 11 -and [string]::IsNullOrEmpty($destinationVersion)) -or ($DestinationVersion -in "SQLServer2000", "SQLServer2005", "SQLServer2008/2008R2")) { + $outsql += "EXEC sys.sp_addsrvrolemember @rolename=N'$roleName', @loginame=N'$userName'" + } + else { + $outsql += "ALTER SERVER ROLE [$roleName] ADD MEMBER [$userName]" + } + } + } + + if ($NoJobs -eq $false) { + $ownedJobs = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -eq $userName } + + foreach ($ownedJob in $ownedJobs) { + $outsql += "`n`rUSE msdb`n" + $outsql += "EXEC msdb.dbo.sp_update_job @job_name=N'$ownedJob', @owner_login_name=N'$userName'" + } + } + + if ($server.VersionMajor -ge 9) { + # These operations are only supported by SQL Server 2005 and above. + # Securables: Connect SQL, View any database, Administer Bulk Operations, etc. + + $perms = $server.EnumServerPermissions($userName) + $outsql += "`n`rUSE master`n" + foreach ($perm in $perms) { + $permState = $perm.permissionstate + $permType = $perm.PermissionType + $grantor = $perm.grantor + + if ($permState -eq "GrantWithGrant") { + $grantWithGrant = "WITH GRANT OPTION" + $permState = "GRANT" + } + else { + $grantWithGrant = $null + } + + $outsql += "$permState $permType TO [$userName] $grantWithGrant AS [$grantor]" + } + + # Credential mapping. Credential removal not currently supported for Syncs. + $loginCredentials = $server.Credentials | Where-Object { $_.Identity -eq $sourceLogin.Name } + foreach ($credential in $loginCredentials) { + $credentialName = $credential.Name + $outsql += "PRINT '$userName is associated with the $credentialName credential'" + } + } + + if ($NoDatabases -eq $false) { + $dbs = $sourceLogin.EnumDatabaseMappings() + + if ($Database) { + $dbs = $dbs | Where-Object { $_.DBName -in $Database } + } + + # Adding database mappings and securables + foreach ($db in $dbs) { + $dbName = $db.dbname + $sourceDb = $server.Databases[$dbName] + $dbUserName = $db.username + + $outsql += "`r`nUSE [$dbName]`n" + try { + $sql = $server.Databases[$dbName].Users[$dbUserName].Script() + $outsql += $sql + } + catch { + Write-Message -Level Warning -Message "User cannot be found in selected database." + } + + # Skipping updating dbowner + + # Database Roles: db_owner, db_datareader, etc + foreach ($role in $sourceDb.Roles) { + if ($role.EnumMembers() -contains $dbUserName) { + $roleName = $role.Name + if (($server.VersionMajor -lt 11 -and [string]::IsNullOrEmpty($destinationVersion)) -or ($DestinationVersion -in "SQLServer2000", "SQLServer2005", "SQLServer2008/2008R2")) { + $outsql += "EXEC sys.sp_addrolemember @rolename=N'$roleName', @membername=N'$dbUserName'" + } + else { + $outsql += "ALTER ROLE [$roleName] ADD MEMBER [$dbUserName]" + } + } + } + + # Connect, Alter Any Assembly, etc + $perms = $sourceDb.EnumDatabasePermissions($dbUserName) + foreach ($perm in $perms) { + $permState = $perm.PermissionState + $permType = $perm.PermissionType + $grantor = $perm.Grantor + + if ($permState -eq "GrantWithGrant") { + $grantWithGrant = "WITH GRANT OPTION" + $permState = "GRANT" + } + else { + $grantWithGrant = $null + } + + $outsql += "$permState $permType TO [$userName] $grantWithGrant AS [$grantor]" + } + } + } + } + } + end { + $sql = $sql | Where-Object { $_ -notlike "CREATE USER [dbo] FOR LOGIN * WITH DEFAULT_SCHEMA=[dbo]" } + + if ($ExcludeGoBatchSeparator) { + $sql = $outsql + } + else { + $sql = $outsql -join "`r`nGO`r`n" + #add the final GO + $sql += "`r`nGO" + } + + if ($FilePath) { + $sql | Out-File -Encoding UTF8 -FilePath $FilePath -Append:$Append -NoClobber:$NoClobber + } + else { + $sql + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Export-SqlLogin + } +} +function Export-DbaPfDataCollectorSetTemplate { + <# + .SYNOPSIS + Exports a new Data Collector Set XML Template. + + .DESCRIPTION + Exports a Data Collector Set XML Template from Get-DbaPfDataCollectorSet. Exports to "$home\Documents\Performance Monitor Templates" by default. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The name of the collector set(s) to export. + + .PARAMETER Path + The path to export the file. Can be .xml or directory. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSetTemplate via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaPfDataCollectorSetTemplate + + .EXAMPLE + Export-DbaPfDataCollectorSetTemplate -ComputerName sql2017 -Path C:\temp\pf + + Exports all data collector sets from to the C:\temp\pf folder. + + .EXAMPLE + Get-DbaPfDataCollectorSet ComputerName sql2017 -CollectorSet 'System Correlation' | Export-DbaPfDataCollectorSetTemplate -Path C:\temp + + Exports the 'System Correlation' data collector set from sql2017 to C:\temp. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [string]$Path = "$home\Documents\Performance Monitor Templates", + [Parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorSet -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet + } + } + + foreach ($object in $InputObject) { + if (-not $object.DataCollectorSetObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorSet." + return + } + + $csname = Remove-InvalidFileNameChars -Name $object.Name + + if ($path.EndsWith(".xml")) { + $filename = $path + } + else { + $filename = "$path\$csname.xml" + if (-not (Test-Path -Path $path)) { + $null = New-Item -Type Directory -Path $path + } + } + Write-Message -Level Verbose -Message "Wrote $csname to $filename." + Set-Content -Path $filename -Value $object.Xml -Encoding Unicode + Get-ChildItem -Path $filename + } + } +} +function Export-DbaScript { + <# + .SYNOPSIS + Exports scripts from SQL Management Objects (SMO) + + .DESCRIPTION + Exports scripts from SQL Management Objects + + .PARAMETER InputObject + A SQL Managment Object such as the one returned from Get-DbaLogin + + .PARAMETER Path + The output filename and location. If no path is specified, one will be created. If the file already exists, the output will be appended. + + .PARAMETER Encoding + Specifies the file encoding. The default is UTF8. + + Valid values are: + -- ASCII: Uses the encoding for the ASCII (7-bit) character set. + -- BigEndianUnicode: Encodes in UTF-16 format using the big-endian byte order. + -- Byte: Encodes a set of characters into a sequence of bytes. + -- String: Uses the encoding type for a string. + -- Unicode: Encodes in UTF-16 format using the little-endian byte order. + -- UTF7: Encodes in UTF-7 format. + -- UTF8: Encodes in UTF-8 format. + -- Unknown: The encoding type is unknown or invalid. The data can be treated as binary. + + .PARAMETER Passthru + Output script to console + + .PARAMETER ScriptingOptionsObject + An SMO Scripting Object that can be used to customize the output - see New-DbaScriptingOption + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed + + .PARAMETER NoClobber + Do not overwrite file + + .PARAMETER Append + Append to file + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Backup, Export + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaScript + + .EXAMPLE + Get-DbaAgentJob -SqlInstance sql2016 | Export-DbaScript + + Exports all jobs on the SQL Server sql2016 instance using a trusted connection - automatically determines filename as .\sql2016-Job-Export-date.sql + + .EXAMPLE + Get-DbaAgentJob -SqlInstance sql2016 | Export-DbaScript -Path C:\temp\export.sql -Append + + Exports all jobs on the SQL Server sql2016 instance using a trusted connection - Will append the output to the file C:\temp\export.sql if it already exists + + .EXAMPLE + Get-DbaAgentJob -SqlInstance sql2016 -Job syspolicy_purge_history, 'Hourly Log Backups' -SqlCredential (Get-Credential sqladmin) | Export-DbaScript -Path C:\temp\export.sql + + Exports only syspolicy_purge_history and 'Hourly Log Backups' to C:temp\export.sql and uses the SQL login "sqladmin" to login to sql2016 + + .EXAMPLE + Get-DbaAgentJob -SqlInstance sql2014 | Export-DbaJob -Passthru | ForEach-Object { $_.Replace('sql2014','sql2016') } | Set-Content -Path C:\temp\export.sql + + Exports jobs and replaces all instances of the servername "sql2014" with "sql2016" then writes to C:\temp\export.sql + + .EXAMPLE + $options = New-DbaScriptingOption + $options.ScriptDrops = $false + $options.WithDependencies = $true + Get-DbaTable -SqlInstance sql2017 -Database PerformanceStore | Export-DbaScript -ScriptingOptionsObject $options + + Exports Agent Jobs with the Scripting Options ScriptDrops set to $false and WithDependencies set to $true. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$InputObject, + [Alias("ScriptingOptionObject")] + [Microsoft.SqlServer.Management.Smo.ScriptingOptions]$ScriptingOptionsObject, + [string]$Path, + [ValidateSet('ASCII', 'BigEndianUnicode', 'Byte', 'String', 'Unicode', 'UTF7', 'UTF8', 'Unknown')] + [string]$Encoding = 'UTF8', + [switch]$Passthru, + [switch]$NoClobber, + [switch]$Append, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $executingUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name + $commandName = $MyInvocation.MyCommand.Name + $timeNow = (Get-Date -uformat "%m%d%Y%H%M%S") + $prefixArray = @() + } + + process { + foreach ($object in $InputObject) { + + $typename = $object.GetType().ToString() + + if ($typename.StartsWith('Microsoft.SqlServer.')) { + $shortype = $typename.Split(".")[-1] + } + else { + Stop-Function -Message "InputObject is of type $typename which is not a SQL Management Object. Only SMO objects are supported." -Category InvalidData -Target $object -Continue + } + + if ($shortype -in "LinkedServer", "Credential", "Login") { + Write-Message -Level Warning -Message "Support for $shortype is limited at this time. No passwords, hashed or otherwise, will be exported if they exist." + } + + # Just gotta add the stuff that Nic Cain added to his script + + if ($shortype -eq "Configuration") { + Write-Message -Level Warning -Message "Support for $shortype is limited at this time." + } + + # Find the server object to pass on to the function + $parent = $object.parent + + do { + if ($parent.Urn.Type -ne "Server") { + $parent = $parent.Parent + } + } + until (($parent.Urn.Type -eq "Server") -or (-not $parent)) + + if (-not $parent) { + Stop-Function -Message "Failed to find valid SMO server object in input: $object." -Category InvalidData -Target $object -Continue + } + + try { + $server = $parent + $serverName = $server.Name.Replace('\', '$') + + if ($ScriptingOptionsObject) { + $scripter = New-Object Microsoft.SqlServer.Management.Smo.Scripter $server + $scripter.Options = $ScriptingOptionsObject + } + + if (!$passthru) { + if ($path) { + $actualPath = $path + } + else { + $actualPath = "$serverName-$shortype-Export-$timeNow.sql" + } + } + + $prefix = "/*`n`tCreated by $executingUser using dbatools $commandName for objects on $serverName at $(Get-Date)`n`tSee https://dbatools.io/$commandName for more information`n*/" + + if ($passthru) { + $prefix | Out-String + } + else { + if ($prefixArray -notcontains $actualPath) { + + if ((Test-Path -Path $actualPath) -and $NoClobber) { + Stop-Function -Message "File already exists. If you want to overwrite it remove the -NoClobber parameter. If you want to append data, please Use -Append parameter." -Target $actualPath -Continue + } + #Only at the first output we use the passed variables Append & NoClobber. For this execution the next ones need to buse -Append + $prefix | Out-File -FilePath $actualPath -Encoding $encoding -Append:$Append -NoClobber:$NoClobber + $prefixArray += $actualPath + } + } + + if ($Pscmdlet.ShouldProcess($env:computername, "Exporting $object from $server to $actualPath")) { + Write-Message -Level Verbose -Message "Exporting $object" + + if ($passthru) { + if ($ScriptingOptionsObject) { + foreach ($script in $scripter.EnumScript($object)) { + $script | Out-String + } + } + else { + $object.Script() | Out-String + } + } + else { + if ($ScriptingOptionsObject) { + foreach ($script in $scripter.EnumScript($object)) { + $script | Out-File -FilePath $actualPath -Encoding $encoding -Append + } + } + else { + $object.Script() | Out-File -FilePath $actualPath -Encoding $encoding -Append + } + } + } + + if (!$passthru) { + Write-Message -Level Output -Message "Exported $object on $($server.Name) to $actualPath" + } + } + catch { + $message = $_.Exception.InnerException.InnerException.InnerException.Message + if (-not $message) { + $message = $_.Exception + } + Stop-Function -Message "Failure on $($server.Name) | $message" -Target $server + } + } + } +} +function Export-DbaSpConfigure { + <# + .SYNOPSIS + Exports advanced sp_configure global configuration options to sql file. + + .DESCRIPTION + Exports advanced sp_configure global configuration options to sql file. + + .PARAMETER SqlInstance + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the path to a file which will contain the sp_configure queries necessary to replicate the configuration settings on another instance. This file is suitable for input into Import-DbaSPConfigure. + + .PARAMETER Whatif + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: SpConfig, Configure, Configuration + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Export-DbaSpConfigure -SqlInstance sourceserver -Path C:\temp\sp_configure.sql + + Exports the SPConfigure settings on sourceserver to the file C:\temp\sp_configure.sql + + .OUTPUTS + File to disk, and string path. + + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [string]$Path, + [PSCredential]$SqlCredential + ) + + begin { + $server = Connect-SqlInstance $sqlinstance $SqlCredential + + if ($server.versionMajor -lt 9) { + Write-Error "Windows 2000 is not supported for sp_configure export." + break + } + + if ($path.length -eq 0) { + $timenow = (Get-Date -uformat "%m%d%Y%H%M%S") + $mydocs = [Environment]::GetFolderPath('MyDocuments') + $path = "$mydocs\$($server.name.replace('\', '$'))-$timenow-sp_configure.sql" + } + + } + + process { + try { + Set-Content -Path $path "EXEC sp_configure 'show advanced options' , 1; RECONFIGURE WITH OVERRIDE" + } + catch { + throw "Can't write to $path" + } + + $server.Configuration.ShowAdvancedOptions.ConfigValue = $true + $server.Configuration.Alter($true) + foreach ($sourceprop in $server.Configuration.Properties) { + $displayname = $sourceprop.DisplayName + $configvalue = $sourceprop.ConfigValue + Add-Content -Path $path "EXEC sp_configure '$displayname' , $configvalue;" + } + Add-Content -Path $path "EXEC sp_configure 'show advanced options' , 0;" + Add-Content -Path $Path "RECONFIGURE WITH OVERRIDE" + $server.Configuration.ShowAdvancedOptions.ConfigValue = $false + $server.Configuration.Alter($true) + return $path + } + + end { + $server.ConnectionContext.Disconnect() + + If ($Pscmdlet.ShouldProcess("console", "Showing finished message")) { + Write-Output "Server configuration export finished" + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Export-SqlSpConfigure + } +} +function Export-DbaUser { + <# + .SYNOPSIS + Exports users creation and its permissions to a T-SQL file or host. + + .DESCRIPTION + Exports users creation and its permissions to a T-SQL file or host. Export includes user, create and add to role(s), database level permissions, object level permissions. + + .PARAMETER SqlInstance + The SQL Server instance name. SQL Server 2000 and above supported. + + .PARAMETER SqlCredential + Allows you to login to servers using alternative credentials + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter + + Windows Authentication will be used if SqlCredential is not specified + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER User + Export only the specified database user(s). If not specified will export all users from the database(s) + + .PARAMETER DestinationVersion + To say to which version the script should be generated. If not specified will use database compatibility level + + .PARAMETER FilePath + The file to write to. + + .PARAMETER NoClobber + Do not overwrite file + + .PARAMETER Append + Append to file + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER ScriptingOptionsObject + A Microsoft.SqlServer.Management.Smo.ScriptingOptions object with the options that you want to use to generate the t-sql script. + You can use the NEw-DbaScriptingOption to generate it. + + .PARAMETER ExcludeGoBatchSeparator + If specified, will NOT script the 'GO' batch separator. + + .NOTES + Tags: User, Export + Author: Claudio Silva (@ClaudioESSilva) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaUser + + .EXAMPLE + Export-DbaUser -SqlInstance sql2005 -FilePath C:\temp\sql2005-users.sql + + Exports SQL for the users in server "sql2005" and writes them to the file "C:\temp\sql2005-users.sql" + + .EXAMPLE + Export-DbaUser -SqlInstance sqlserver2014a $scred -FilePath C:\temp\users.sql -Append + + Authenticates to sqlserver2014a using SQL Authentication. Exports all users to C:\temp\users.sql, and appends to the file if it exists. If not, the file will be created. + + .EXAMPLE + Export-DbaUser -SqlInstance sqlserver2014a -User User1, User2 -FilePath C:\temp\users.sql + + Exports ONLY users User1 and User2 fron sqlsever2014a to the file C:\temp\users.sql + + .EXAMPLE + Export-DbaUser -SqlInstance sqlserver2008 -User User1 -FilePath C:\temp\users.sql -DestinationVersion SQLServer2016 + + Exports user User1 fron sqlsever2008 to the file C:\temp\users.sql with sintax to run on SQL Server 2016 + + .EXAMPLE + Export-DbaUser -SqlInstance sqlserver2008 -Database db1,db2 -FilePath C:\temp\users.sql + + Exports ONLY users from db1 and db2 database on sqlserver2008 server, to the C:\temp\users.sql file. + + .EXAMPLE + $options = New-DbaScriptingOption + $options.ScriptDrops = $false + $options.WithDependencies = $true + + Export-DbaUser -SqlInstance sqlserver2008 -Database db1,db2 -FilePath C:\temp\users.sql -ScriptingOptionsObject $options + + Exports ONLY users from db1 and db2 database on sqlserver2008 server, to the C:\temp\users.sql file. + It will not script drops but will script dependencies. + + .EXAMPLE + Export-DbaUser -SqlInstance sqlserver2008 -Database db1,db2 -FilePath C:\temp\users.sql -ExcludeGoBatchSeparator + + Exports ONLY users from db1 and db2 database on sqlserver2008 server, to the C:\temp\users.sql file without the 'GO' batch separator. + + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + [OutputType([String])] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$User, + [ValidateSet('SQLServer2000', 'SQLServer2005', 'SQLServer2008/2008R2', 'SQLServer2012', 'SQLServer2014', 'SQLServer2016', 'SQLServer2017')] + [string]$DestinationVersion, + [Alias("OutFile", "Path", "FileName")] + [string]$FilePath, + [Alias("NoOverwrite")] + [switch]$NoClobber, + [switch]$Append, + [Alias('Silent')] + [switch]$EnableException, + [Microsoft.SqlServer.Management.Smo.ScriptingOptions]$ScriptingOptionsObject = $null, + [switch]$ExcludeGoBatchSeparator + ) + + begin { + if ($FilePath) { + if ($FilePath -notlike "*\*") { $FilePath = ".\$filepath" } + $directory = Split-Path $FilePath + $exists = Test-Path $directory + + if ($exists -eq $false) { + Stop-Function -Message "Parent directory $directory does not exist" + return + } + + Write-Message -Level Output -Message "Attempting to connect to SQL Servers.." + } + + $outsql = @() + + $versions = @{ + 'SQLServer2000' = 'Version80' + 'SQLServer2005' = 'Version90' + 'SQLServer2008/2008R2' = 'Version100' + 'SQLServer2012' = 'Version110' + 'SQLServer2014' = 'Version120' + 'SQLServer2016' = 'Version130' + 'SQLServer2017' = 'Version140' + } + + $versionName = @{ + 'Version80' = 'SQLServer2000' + 'Version90' = 'SQLServer2005' + 'Version100' = 'SQLServer2008/2008R2' + 'Version110' = 'SQLServer2012' + 'Version120' = 'SQLServer2014' + 'Version130' = 'SQLServer2016' + 'Version140' = 'SQLServer2017' + } + + } + process { + if (Test-FunctionInterrupt) { return } + + try { + Write-Message -Level Verbose -Message "Connecting to $sqlinstance" + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (!$database) { + $databases = $server.Databases | Where-Object { $ExcludeDatabase -notcontains $_.Name -and $_.IsAccessible -eq $true } + } + else { + if ($pipedatabase) { + $source = $pipedatabase[0].parent.name + $databases = $pipedatabase.name + } + else { + $databases = $server.Databases | Where-Object { $_.IsAccessible -eq $true -and ($database -contains $_.Name) } + } + } + + if ($exclude) { + $databases = $databases | Where-Object Name -notin $ExcludeDatabase + } + + if (@($databases).Count -gt 0) { + + #Database Permissions + foreach ($db in $databases) { + if ([string]::IsNullOrEmpty($destinationVersion)) { + #Get compatibility level for scripting the objects + $scriptVersion = $db.CompatibilityLevel + } + else { + $scriptVersion = $versions[$destinationVersion] + } + $versionNameDesc = $versionName[$scriptVersion.ToString()] + + #If not passed create new ScriptingOption. Otherwise use the one that was passed + if ($null -eq $ScriptingOptionsObject) { + $ScriptingOptionsObject = New-DbaScriptingOption + $ScriptingOptionsObject.TargetServerVersion = [Microsoft.SqlServer.Management.Smo.SqlServerVersion]::$scriptVersion + $ScriptingOptionsObject.AllowSystemObjects = $false + $ScriptingOptionsObject.IncludeDatabaseRoleMemberships = $true + $ScriptingOptionsObject.ContinueScriptingOnError = $false + $ScriptingOptionsObject.IncludeDatabaseContext = $false + $ScriptingOptionsObject.IncludeIfNotExists = $true + } + + Write-Message -Level Output -Message "Validating users on database $db" + + if ($User.Count -eq 0) { + $users = $db.Users | Where-Object { $_.IsSystemObject -eq $false -and $_.Name -notlike "##*" } + } + else { + if ($pipedatabase) { + $source = $pipedatabase[3].parent.name + $users = $pipedatabase.name + } + else { + $users = $db.Users | Where-Object { $User -contains $_.Name -and $_.IsSystemObject -eq $false -and $_.Name -notlike "##*" } + } + } + # Store roles between users so if we hit the same one we dont create it again + $roles = @() + if ($users.Count -gt 0) { + foreach ($dbuser in $users) { + Write-Message -Level Output -Message "Generating script for user $dbuser" + + #setting database + $outsql += "USE [" + $db.Name + "]" + + try { + #Fixed Roles #Dependency Issue. Create Role, before add to role. + foreach ($rolePermission in ($db.Roles | Where-Object { $_.IsFixedRole -eq $false })) { + foreach ($rolePermissionScript in $rolePermission.Script($ScriptingOptionsObject)) { + if ($rolePermission.ToString() -notin $roles) { + $roles += , $rolePermission.ToString() + $outsql += "$($rolePermissionScript.ToString())" + } + + } + } + + #Database Create User(s) and add to Role(s) + foreach ($dbUserPermissionScript in $dbuser.Script($ScriptingOptionsObject)) { + if ($dbuserPermissionScript.Contains("sp_addrolemember")) { + $execute = "EXEC " + } + else { + $execute = "" + } + $outsql += "$execute$($dbUserPermissionScript.ToString())" + } + + #Database Permissions + foreach ($databasePermission in $db.EnumDatabasePermissions() | Where-Object { @("sa", "dbo", "information_schema", "sys") -notcontains $_.Grantee -and $_.Grantee -notlike "##*" -and ($dbuser.Name -contains $_.Grantee) }) { + if ($databasePermission.PermissionState -eq "GrantWithGrant") { + $withGrant = " WITH GRANT OPTION" + $grantDatabasePermission = 'GRANT' + } + else { + $withGrant = " " + $grantDatabasePermission = $databasePermission.PermissionState.ToString().ToUpper() + } + + $outsql += "$($grantDatabasePermission) $($databasePermission.PermissionType) TO [$($databasePermission.Grantee)]$withGrant AS [$($databasePermission.Grantor)];" + } + + #Database Object Permissions + # NB: This is a bit of a mess for a couple of reasons + # 1. $db.EnumObjectPermissions() doesn't enumerate all object types + # 2. Some (x)Collection types can have EnumObjectPermissions() called + # on them directly (e.g. AssemblyCollection); others can't (e.g. + # ApplicationRoleCollection). Those that can't we iterate the + # collection explicitly and add each object's permission. + + $perms = New-Object System.Collections.ArrayList + + $null = $perms.AddRange($db.EnumObjectPermissions($dbuser.Name)) + + foreach ($item in $db.ApplicationRoles) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.Assemblies) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.Certificates) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.DatabaseRoles) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.FullTextCatalogs) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.FullTextStopLists) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.SearchPropertyLists) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.ServiceBroker.MessageTypes) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.RemoteServiceBindings) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.ServiceBroker.Routes) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.ServiceBroker.ServiceContracts) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.ServiceBroker.Services) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + if ($scriptVersion -ne "Version80") { + foreach ($item in $db.AsymmetricKeys) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + } + + foreach ($item in $db.SymmetricKeys) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($item in $db.XmlSchemaCollections) { + $null = $perms.AddRange($item.EnumObjectPermissions($dbuser.Name)) + } + + foreach ($objectPermission in $perms | Where-Object { @("sa", "dbo", "information_schema", "sys") -notcontains $_.Grantee -and $_.Grantee -notlike "##*" -and $_.Grantee -eq $dbuser.Name }) { + switch ($objectPermission.ObjectClass) { + 'ApplicationRole' { + $object = 'APPLICATION ROLE::[{0}]' -f $objectPermission.ObjectName + } + 'AsymmetricKey' { + $object = 'ASYMMETRIC KEY::[{0}]' -f $objectPermission.ObjectName + } + 'Certificate' { + $object = 'CERTIFICATE::[{0}]' -f $objectPermission.ObjectName + } + 'DatabaseRole' { + $object = 'ROLE::[{0}]' -f $objectPermission.ObjectName + } + 'FullTextCatalog' { + $object = 'FULLTEXT CATALOG::[{0}]' -f $objectPermission.ObjectName + } + 'FullTextStopList' { + $object = 'FULLTEXT STOPLIST::[{0}]' -f $objectPermission.ObjectName + } + 'MessageType' { + $object = 'Message Type::[{0}]' -f $objectPermission.ObjectName + } + 'ObjectOrColumn' { + if ($scriptVersion -ne "Version80") { + $object = 'OBJECT::[{0}].[{1}]' -f $objectPermission.ObjectSchema, $objectPermission.ObjectName + if ($null -ne $objectPermission.ColumnName) { + $object += '([{0}])' -f $objectPermission.ColumnName + } + } + #At SQL Server 2000 OBJECT did not exists + else { + $object = '[{0}].[{1}]' -f $objectPermission.ObjectSchema, $objectPermission.ObjectName + } + } + 'RemoteServiceBinding' { + $object = 'REMOTE SERVICE BINDING::[{0}]' -f $objectPermission.ObjectName + } + 'Schema' { + $object = 'SCHEMA::[{0}]' -f $objectPermission.ObjectName + } + 'SearchPropertyList' { + $object = 'SEARCH PROPERTY LIST::[{0}]' -f $objectPermission.ObjectName + } + 'Service' { + $object = 'SERVICE::[{0}]' -f $objectPermission.ObjectName + } + 'ServiceContract' { + $object = 'CONTRACT::[{0}]' -f $objectPermission.ObjectName + } + 'ServiceRoute' { + $object = 'ROUTE::[{0}]' -f $objectPermission.ObjectName + } + 'SqlAssembly' { + $object = 'ASSEMBLY::[{0}]' -f $objectPermission.ObjectName + } + 'SymmetricKey' { + $object = 'SYMMETRIC KEY::[{0}]' -f $objectPermission.ObjectName + } + 'User' { + $object = 'USER::[{0}]' -f $objectPermission.ObjectName + } + 'UserDefinedType' { + $object = 'TYPE::[{0}].[{1}]' -f $objectPermission.ObjectSchema, $objectPermission.ObjectName + } + 'XmlNamespace' { + $object = 'XML SCHEMA COLLECTION::[{0}]' -f $objectPermission.ObjectName + } + } + + if ($objectPermission.PermissionState -eq "GrantWithGrant") { + $withGrant = " WITH GRANT OPTION" + $grantObjectPermission = 'GRANT' + } + else { + $withGrant = " " + $grantObjectPermission = $objectPermission.PermissionState.ToString().ToUpper() + } + + $outsql += "$grantObjectPermission $($objectPermission.PermissionType) ON $object TO [$($objectPermission.Grantee)]$withGrant AS [$($objectPermission.Grantor)];" + } + + } + catch { + Stop-Function -Message "This user may be using functionality from $($versionName[$db.CompatibilityLevel.ToString()]) that does not exist on the destination version ($versionNameDesc)." -Continue -InnerErrorRecord $_ -Target $db + } + } + } + else { + Write-Message -Level Output -Message "No users found on database '$db'" + } + + #reset collection + $users = $null + } + } + else { + Write-Message -Level Output -Message "No users found on instance '$server'" + } + } + + end { + if (Test-FunctionInterrupt) { return } + + if ($ExcludeGoBatchSeparator) { + $sql = $outsql + } + else { + $sql = $outsql -join "`r`nGO`r`n" + #add the final GO + $sql += "`r`nGO" + } + + if ($FilePath) { + $sql | Out-File -Encoding UTF8 -FilePath $FilePath -Append:$Append -NoClobber:$NoClobber + } + else { + $sql + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Export-SqlUser + } +} +function Export-DbaXECsv { + <# + .SYNOPSIS + Exports Extended Events to a CSV file. + + .DESCRIPTION + Exports Extended Events to a CSV file. + + .PARAMETER Path + Specifies the InputObject to the output CSV file + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER InputObject + Allows Piping + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Author: Gianluca Sartori (@spaghettidba) + + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/Export-DbaXECsv + + .EXAMPLE + Get-ChildItem -Path C:\temp\sample.xel | Export-DbaXECsv -Path c:\temp\sample.csv + + Writes Extended Events data to the file "C:\temp\events.csv". + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2014 -Session deadlocks | Export-DbaXECsv -Path c:\temp\events.csv + + Writes Extended Events data to the file "C:\temp\events.csv". + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('FullName')] + [object[]]$InputObject, + [parameter(Mandatory)] + [string]$Path, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + + function Get-FileFromXE ($InputObject) { + if ($InputObject.TargetFile) { + if ($InputObject.TargetFile.Length -eq 0) { + Stop-Function -Message "This session does not have an associated Target File." + return + } + + $instance = [dbainstance]$InputObject.ComputerName + + if ($instance.IsLocalHost) { + $xelpath = $InputObject.TargetFile + } + else { + $xelpath = $InputObject.RemoteTargetFile + } + + if ($xelpath -notmatch ".xel") { + $xelpath = "$xelpath*.xel" + } + + try { + Get-ChildItem -Path $xelpath -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } + } + } + process { + if (Test-FunctionInterrupt) { return } + + $getfiles = Get-FileFromXE $InputObject + + if ($getfiles) { + $InputObject += $getfiles + } + + foreach ($file in $InputObject) { + if ($file -is [System.String]) { + $currentfile = $file + } + elseif ($file -is [System.IO.FileInfo]) { + $currentfile = $file.FullName + } + elseif ($file -is [Microsoft.SqlServer.Management.XEvent.Session]) { + # it was taken care of above + continue + } + else { + Stop-Function -Message "Unsupported file type." + return + } + + $accessible = Test-Path -Path $currentfile + $whoami = whoami + + if (-not $accessible) { + if ($file.Status -eq "Stopped") { continue } + Stop-Function -Continue -Message "$currentfile cannot be accessed from $($env:COMPUTERNAME). Does $whoami have access?" + } + + if (-not (Test-Path $Path)) { + if ([String]::IsNullOrEmpty([IO.Path]::GetExtension($Path))) { + New-Item $Path -ItemType directory | Out-Null + $outDir = $Path + $outFile = [IO.Path]::GetFileNameWithoutExtension($currentfile) + ".csv" + } + else { + $outDir = [IO.Path]::GetDirectoryName($Path) + $outFile = [IO.Path]::GetFileName($Path) + } + } + else { + if ((Get-Item $Path) -is [System.IO.DirectoryInfo]) { + $outDir = $Path + $outFile = [IO.Path]::GetFileNameWithoutExtension($currentfile) + ".csv" + } + else { + $outDir = [IO.Path]::GetDirectoryName($Path) + $outFile = [IO.Path]::GetFileName($Path) + } + } + + $adapter = New-Object XESmartTarget.Core.Utils.XELFileCSVAdapter + $adapter.InputFile = $currentfile + $adapter.OutputFile = (Join-Path $outDir $outFile) + + try { + $adapter.Convert() + $file = Get-ChildItem -Path $adapter.OutputFile + + if ($file.Length -eq 0) { + Remove-Item -Path $adapter.OutputFile + } + else { + $file + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } + } +} +function Export-DbaXESessionTemplate { + <# + .SYNOPSIS + Exports an XESession XML Template. + + .DESCRIPTION + Exports an XESession XML Template either from the dbatools repository or a file you specify. Exports to "$home\Documents\SQL Server Management Studio\Templates\XEventTemplates" by default + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + The Name of the session(s) to export. + + .PARAMETER Path + The path to export the file into. Can be .xml or directory. + + .PARAMETER InputObject + Specifies an XE Session output by Get-DbaXESession. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Export-DbaXESessionTemplate + + .EXAMPLE + Export-DbaXESessionTemplate -SqlInstance sql2017 -Path C:\temp\xe + + Exports XE Session Template to the C:\temp\xe folder. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2017 -Session session_health | Export-DbaXESessionTemplate -Path C:\temp + + Returns a new XE Session object from sql2017 then adds an event, an action then creates it. + + #> + [CmdletBinding()] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Session, + [string]$Path = "$home\Documents\SQL Server Management Studio\Templates\XEventTemplates", + [Parameter(ValueFromPipeline)] + [Microsoft.SqlServer.Management.XEvent.Session[]]$InputObject, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $InputObject += Get-DbaXESession -SqlInstance $instance -SqlCredential $SqlCredential -Session $Session -EnableException + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + } + + foreach ($xes in $InputObject) { + $xesname = Remove-InvalidFileNameChars -Name $xes.Name + + if (-not (Test-Path -Path $Path)) { + Stop-Function -Message "$Path does not exist." -Target $Path + } + + if ($path.EndsWith(".xml")) { + $filename = $path + } + else { + $filename = "$path\$xesname.xml" + } + Write-Message -Level Verbose -Message "Wrote $xesname to $filename" + [Microsoft.SqlServer.Management.XEvent.XEStore]::SaveSessionToTemplate($xes, $filename, $true) + Get-ChildItem -Path $filename + } + } +} +function Find-DbaAgentJob { + <# + .SYNOPSIS + Find-DbaAgentJob finds agent job/s that fit certain search filters. + + .DESCRIPTION + This command filters SQL Agent jobs giving the DBA a list of jobs that may need attention or could possibly be options for removal. + + .PARAMETER SqlInstance + The SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER JobName + Filter agent jobs to only the name(s) you list. + Supports regular expression (e.g. MyJob*) being passed in. + + .PARAMETER ExcludeJobName + Allows you to enter an array of agent job names to ignore + + .PARAMETER StepName + Filter based on StepName. + Supports regular expression (e.g. MyJob*) being passed in. + + .PARAMETER LastUsed + Find all jobs that havent ran in the INT number of previous day(s) + + .PARAMETER IsDisabled + Find all jobs that are disabled + + .PARAMETER IsFailed + Find all jobs that have failed + + .PARAMETER IsNotScheduled + Find all jobs with no schedule assigned + + .PARAMETER IsNoEmailNotification + Find all jobs without email notification configured + + .PARAMETER Category + Filter based on agent job categories + + .PARAMETER Owner + Filter based on owner of the job/s + + .PARAMETER Since + Datetime object used to narrow the results to a date + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job + Author: Stephen Bennett (https://sqlnotesfromtheunderground.wordpress.com/) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaAgentJob + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01 -JobName backup* + + Returns all agent job(s) that have backup in the name + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01, Dev02 -JobName Mybackup + + Returns all agent job(s) that are named exactly Mybackup + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01 -LastUsed 10 + + Returns all agent job(s) that have not ran in 10 days + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01 -IsDisabled -IsNoEmailNotification -IsNotScheduled + + Returns all agent job(s) that are either disabled, have no email notification or don't have a schedule. returned with detail + + .EXAMPLE + $servers | Find-DbaAgentJob -IsFailed | Start-DbaAgentJob + + Finds all failed job then starts them. Consider using a -WhatIf at the end of Start-DbaAgentJob to see what it'll do first + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01 -LastUsed 10 -Exclude "Yearly - RollUp Workload", "SMS - Notification" + + Returns all agent jobs that havent ran in the last 10 ignoring jobs "Yearly - RollUp Workload" and "SMS - Notification" + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01 -Category "REPL-Distribution", "REPL-Snapshot" -Detailed | Format-Table -AutoSize -Wrap + + Returns all job/s on Dev01 that are in either category "REPL-Distribution" or "REPL-Snapshot" with detailed output + + .EXAMPLE + Find-DbaAgentJob -SqlInstance Dev01, Dev02 -IsFailed -Since '7/1/2016 10:47:00' + + Returns all agent job(s) that have failed since July of 2016 (and still have history in msdb) + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance CMSServer -Group Production | Find-DbaAgentJob -Disabled -IsNotScheduled | Format-Table -AutoSize -Wrap + + Queries CMS server to return all SQL instances in the Production folder and then list out all agent jobs that have either been disabled or have no schedule. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [Alias("Name")] + [string[]]$JobName, + [string[]]$ExcludeJobName, + [string[]]$StepName, + [int]$LastUsed, + [Alias("Disabled")] + [switch]$IsDisabled, + [Alias("Failed")] + [switch]$IsFailed, + [Alias("NoSchedule")] + [switch]$IsNotScheduled, + [Alias("NoEmailNotification")] + [switch]$IsNoEmailNotification, + [string[]]$Category, + [string]$Owner, + [datetime]$Since, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ($IsFailed, [boolean]$JobName, [boolean]$StepName, [boolean]$LastUsed.ToString(), $IsDisabled, $IsNotScheduled, $IsNoEmailNotification, [boolean]$Category, [boolean]$Owner, [boolean]$ExcludeJobName -notcontains $true) { + Stop-Function -Message "At least one search term must be specified" + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Running Scan on: $instance" + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $jobs = $server.JobServer.jobs + $output = @() + + if ($IsFailed) { + Write-Message -Level Verbose -Message "Checking for failed jobs." + $output += $jobs | Where-Object LastRunOutcome -eq "Failed" + } + + if ($JobName) { + Write-Message -Level Verbose -Message "Retrieving jobs by their name." + $output += Get-JobList -SqlInstance $server -JobFilter $JobName + } + + if ($StepName) { + Write-Message -Level Verbose -Message "Retrieving jobs by their step names." + $output += Get-JobList -SqlInstance $server -StepFilter $StepName + } + + if ($LastUsed) { + $DaysBack = $LastUsed * -1 + $SinceDate = (Get-date).AddDays($DaysBack) + Write-Message -Level Verbose -Message "Finding job/s not ran in last $LastUsed days" + $output += $jobs | Where-Object { $_.LastRunDate -le $SinceDate } + } + + if ($IsDisabled) { + Write-Message -Level Verbose -Message "Finding job/s that are disabled" + $output += $jobs | Where-Object IsEnabled -eq $false + } + + if ($IsNotScheduled) { + Write-Message -Level Verbose -Message "Finding job/s that have no schedule defined" + $output += $jobs | Where-Object HasSchedule -eq $false + } + if ($IsNoEmailNotification) { + Write-Message -Level Verbose -Message "Finding job/s that have no email operator defined" + $output += $jobs | Where-Object { [string]::IsNullOrEmpty($_.OperatorToEmail) -eq $true } + } + + if ($Category) { + Write-Message -Level Verbose -Message "Finding job/s that have the specified category defined" + $output += $jobs | Where-Object { $Category -contains $_.Category } + } + + if ($Owner) { + Write-Message -Level Verbose -Message "Finding job/s with owner critera" + if ($Owner -match "-") { + $OwnerMatch = $Owner -replace "-", "" + Write-Message -Level Verbose -Message "Checking for jobs that NOT owned by: $OwnerMatch" + $output += $server.JobServer.jobs | Where-Object { $OwnerMatch -notcontains $_.OwnerLoginName } + } + else { + Write-Message -Level Verbose -Message "Checking for jobs that are owned by: $owner" + $output += $server.JobServer.jobs | Where-Object { $Owner -contains $_.OwnerLoginName } + } + } + + if ($Exclude) { + Write-Message -Level Verbose -Message "Excluding job/s based on Exclude" + $output = $output | Where-Object { $Exclude -notcontains $_.Name } + } + + if ($Since) { + #$Since = $Since.ToString("yyyy-MM-dd HH:mm:ss") + Write-Message -Level Verbose -Message "Getting only jobs whose LastRunDate is greater than or equal to $since" + $output = $output | Where-Object { $_.LastRunDate -ge $since } + } + + $jobs = $output | Select-Object -Unique + + foreach ($job in $jobs) { + Add-Member -Force -InputObject $job -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $job -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $job -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $job -MemberType NoteProperty -Name JobName -value $job.Name + + + Select-DefaultView -InputObject $job -Property ComputerName, InstanceName, SqlInstance, Name, Category, OwnerLoginName, CurrentRunStatus, CurrentRunRetryAttempt, 'IsEnabled as Enabled', LastRunDate, LastRunOutcome, DateCreated, HasSchedule, OperatorToEmail, 'DateCreated as CreateDate' + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Find-DbaBackup { + <# + .SYNOPSIS + Finds SQL Server backups on disk. + + .DESCRIPTION + Provides all of the same functionality for finding SQL backups to remove from disk as a standard maintenance plan would. + + As an addition you have the ability to check the Archive bit on files before deletion. This will allow you to ensure backups have been archived to your archive location before removal. + + .PARAMETER Path + Specifies the name of the base level folder to search for backup files. + + .PARAMETER BackupFileExtension + Specifies the filename extension of the backup files you wish to find (typically 'bak', 'trn' or 'log'). Do not include the period. + + .PARAMETER RetentionPeriod + Specifies the retention period for backup files. Correct format is ##U. + + ## is the retention value and must be an integer value + U signifies the units where the valid units are: + h = hours + d = days + w = weeks + m = months + + Formatting Examples: + '48h' = 48 hours + '7d' = 7 days + '4w' = 4 weeks + '1m' = 1 month + + .PARAMETER CheckArchiveBit + If this switch is enabled, the filesystem Archive bit is checked. + If this bit is set (which translates to "it has not been backed up to another location yet"), the file won't be included. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup + Author: Chris Sommer, @cjsommer, www.cjsommer.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaBackup + + .EXAMPLE + Find-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h + + '*.trn' files in 'C:\MSSQL\SQL Backup\' and all subdirectories that are more than 48 hours old will be included. + + .EXAMPLE + Find-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 7d -CheckArchiveBit + + '*.bak' files in 'C:\MSSQL\Backup\' and all subdirectories that are more than 7 days old will be included, but only if the files have been backed up to another location as verified by checking the Archive bit. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, HelpMessage = "Full path to the root level backup folder (ex. 'C:\SQL\Backups'")] + [Alias("BackupFolder")] + [string]$Path, + [parameter(Mandatory = $true, HelpMessage = "Backup File extension to remove (ex. bak, trn, dif)")] + [string]$BackupFileExtension , + [parameter(Mandatory = $true, HelpMessage = "Backup retention period. (ex. 24h, 7d, 4w, 6m)")] + [string]$RetentionPeriod , + [parameter(Mandatory = $false)] + [switch]$CheckArchiveBit = $false , + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + ### Local Functions + function Convert-UserFriendlyRetentionToDatetime { + [cmdletbinding()] + param ( + [string]$UserFriendlyRetention + ) + + <# + Convert a user friendly retention value into a datetime. + The last character of the string will indicate units (validated) + Valid units are: (h = hours, d = days, w = weeks, m = months) + + The preceeding characters are the value and must be an integer (validated) + + Examples: + '48h' = 48 hours + '7d' = 7 days + '4w' = 4 weeks + '1m' = 1 month + #> + + [int]$Length = ($UserFriendlyRetention).Length + $Value = ($UserFriendlyRetention).Substring(0, $Length - 1) + $Units = ($UserFriendlyRetention).Substring($Length - 1, 1) + + # Validate that $Units is an accepted unit of measure + if ( $Units -notin @('h', 'd', 'w', 'm') ) { + throw "RetentionPeriod '$UserFriendlyRetention' units invalid! See Get-Help for correct formatting and examples." + } + + # Validate that $Value is an INT + if ( ![int]::TryParse($Value, [ref]"") ) { + throw "RetentionPeriod '$UserFriendlyRetention' format invalid! See Get-Help for correct formatting and examples." + } + + switch ($Units) { + 'h' { $UnitString = 'Hours'; [datetime]$ReturnDatetime = (Get-Date).AddHours( - $Value) } + 'd' { $UnitString = 'Days'; [datetime]$ReturnDatetime = (Get-Date).AddDays( - $Value) } + 'w' { $UnitString = 'Weeks'; [datetime]$ReturnDatetime = (Get-Date).AddDays( - $Value * 7) } + 'm' { $UnitString = 'Months'; [datetime]$ReturnDatetime = (Get-Date).AddMonths( - $Value) } + } + $ReturnDatetime + } + + # Validations + # Ensure BackupFileExtension does not begin with a . + if ($BackupFileExtension -match "^[.]") { + Write-Message -Level Warning -Message "Parameter -BackupFileExtension begins with a period '$BackupFileExtension'. A period is automatically prepended to -BackupFileExtension and need not be passed in." + } + # Ensure Path is a proper path + if (!(Test-Path $Path -PathType 'Container')) { + Stop-Function -Message "$Path not found" + } + + } + process { + if (Test-FunctionInterrupt) { return } + # Process stuff + Write-Message -Message "Finding backups on $Path" -Level Verbose + # Convert Retention Value to an actual DateTime + try { + $RetentionDate = Convert-UserFriendlyRetentionToDatetime -UserFriendlyRetention $RetentionPeriod + Write-Message -Message "Backup Retention Date set to $RetentionDate" -Level Verbose + } + catch { + Stop-Function -Message "Failed to interpret retention time!" -ErrorRecord $_ + } + + # Filter out unarchived files if -CheckArchiveBit parameter is used + if ($CheckArchiveBit) { + Write-Message -Message "Removing only archived files." -Level Verbose + filter DbaArchiveBitFilter { + if ($_.Attributes -notmatch "Archive") { + $_ + } + } + } + else { + filter DbaArchiveBitFilter { + $_ + } + } + # Enumeration may take a while. Without resorting to "esoteric" file listing facilities + # and given we need to fetch at least the LastWriteTime, let's just use "streaming" processing + # here to avoid issues like described in #970 + Get-ChildItem $Path -Filter "*.$BackupFileExtension" -File -Recurse -ErrorAction SilentlyContinue -ErrorVariable EnumErrors | + Where-Object LastWriteTime -lt $RetentionDate | DbaArchiveBitFilter + if ($EnumErrors) { + Write-Message "Errors encountered enumerating files." -Level Warning -ErrorRecord $EnumErrors + } + } +} +#ValidationTags#Messaging# +function Find-DbaCommand { + <# + .SYNOPSIS + Finds dbatools commands searching through the inline help text + + .DESCRIPTION + Finds dbatools commands searching through the inline help text, building a consolidated json index and querying it because Get-Help is too slow + + .PARAMETER Tag + Finds all commands tagged with this auto-populated tag + + .PARAMETER Author + Finds all commands tagged with this author + + .PARAMETER MinimumVersion + Finds all commands tagged with this auto-populated minimum version + + .PARAMETER MaximumVersion + Finds all commands tagged with this auto-populated maximum version + + .PARAMETER Rebuild + Rebuilds the index + + .PARAMETER Pattern + Searches help for all commands in dbatools for the specified pattern and displays all results + + .PARAMETER Confirm + Confirms overwrite of index + + .PARAMETER WhatIf + Displays what would happen if the command is run + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Find,Help,Command + Author: Simone Bizzotto + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaCommand + + .EXAMPLE + Find-DbaCommand "snapshot" + + For lazy typers: finds all commands searching the entire help for "snapshot" + + .EXAMPLE + Find-DbaCommand -Pattern "snapshot" + + For rigorous typers: finds all commands searching the entire help for "snapshot" + + .EXAMPLE + Find-DbaCommand -Tag copy + + Finds all commands tagged with "copy" + + .EXAMPLE + Find-DbaCommand -Tag copy,user + + Finds all commands tagged with BOTH "copy" and "user" + + .EXAMPLE + Find-DbaCommand -Author chrissy + + Finds every command whose author contains our beloved "chrissy" + + .EXAMPLE + Find-DbaCommand -Author chrissy -Tag copy + + Finds every command whose author contains our beloved "chrissy" and it tagged as "copy" + + .EXAMPLE + Find-DbaCommand -Pattern snapshot -Rebuild + + Finds all commands searching the entire help for "snapshot", rebuilding the index (good for developers) + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [String]$Pattern, + [String[]]$Tag, + [String]$Author, + [String]$MinimumVersion, + [String]$MaximumVersion, + [switch]$Rebuild, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $tagsRex = ([regex]'(?m)^[\s]{0,15}Tags:(.*)$') + $authorRex = ([regex]'(?m)^[\s]{0,15}Author:(.*)$') + $minverRex = ([regex]'(?m)^[\s]{0,15}MinimumVersion:(.*)$') + $maxverRex = ([regex]'(?m)^[\s]{0,15}MaximumVersion:(.*)$') + + function Get-DbaHelp([String]$commandName) { + $thishelp = Get-Help $commandName -Full + $thebase = @{ } + $thebase.CommandName = $commandName + $thebase.Name = $thishelp.Name + + ## fetch the description + $thebase.Description = $thishelp.Description.Text + + ## fetch examples + $thebase.Examples = $thishelp.Examples | Out-String -Width 120 + + ## fetch help link + $thebase.Links = ($thishelp.relatedLinks).NavigationLink.Uri + + ## fetch the synopsis + $thebase.Synopsis = $thishelp.Synopsis + + ## store notes + $as = $thishelp.AlertSet | Out-String -Width 120 + + ## fetch the tags + $tags = $tagsrex.Match($as).Groups[1].Value + if ($tags) { + $thebase.Tags = $tags.Split(',').Trim() + } + ## fetch the author + $author = $authorRex.Match($as).Groups[1].Value + if ($author) { + $thebase.Author = $author.Trim() + } + + ## fetch MinimumVersion + $MinimumVersion = $minverRex.Match($as).Groups[1].Value + if ($MinimumVersion) { + $thebase.MinimumVersion = $MinimumVersion.Trim() + } + + ## fetch MaximumVersion + $MaximumVersion = $maxverRex.Match($as).Groups[1].Value + if ($MaximumVersion) { + $thebase.MaximumVersion = $MaximumVersion.Trim() + } + + [pscustomobject]$thebase + } + + function Get-DbaIndex() { + if ($Pscmdlet.ShouldProcess($dest, "Recreating index")) { + $dbamodule = Get-Module -Name dbatools + $allCommands = $dbamodule.ExportedCommands.Values | Where-Object CommandType -EQ 'Function' + + $helpcoll = New-Object System.Collections.Generic.List[System.Object] + foreach ($command in $allCommands) { + $x = Get-DbaHelp "$command" + $helpcoll.Add($x) + } + # $dest = Get-DbaConfigValue -Name 'Path.TagCache' -Fallback "$(Resolve-Path $PSScriptRoot\..)\dbatools-index.json" + $dest = "$moduleDirectory\bin\dbatools-index.json" + $helpcoll | ConvertTo-Json | Out-File $dest -Encoding UTF8 + } + } + + $moduleDirectory = (Get-Module -Name dbatools).ModuleBase + } + process { + $Pattern = $Pattern.TrimEnd("s") + $idxFile = "$moduleDirectory\bin\dbatools-index.json" + if (!(Test-Path $idxFile) -or $Rebuild) { + Write-Message -Level Verbose -Message "Rebuilding index into $idxFile" + $swRebuild = [system.diagnostics.stopwatch]::StartNew() + Get-DbaIndex + Write-Message -Level Verbose -Message "Rebuild done in $($swRebuild.ElapsedMilliseconds)ms" + } + $consolidated = Get-Content -Raw $idxFile | ConvertFrom-Json + $result = $consolidated + if ($Pattern.Length -gt 0) { + $result = $result | Where-Object { $_.PsObject.Properties.Value -like "*$Pattern*" } + } + + if ($Tag.Length -gt 0) { + foreach ($t in $Tag) { + $result = $result | Where-Object Tags -Contains $t + } + } + + if ($Author.Length -gt 0) { + $result = $result | Where-Object Author -Like "*$Author*" + } + + if ($MinimumVersion.Length -gt 0) { + $result = $result | Where-Object MinimumVersion -GE $MinimumVersion + } + + if ($MaximumVersion.Length -gt 0) { + $result = $result | Where-Object MaximumVersion -LE $MaximumVersion + } + + Select-DefaultView -InputObject $result -Property CommandName, Synopsis + } +} +#ValidationTags#Messaging# +function Find-DbaDatabase { + <# + .SYNOPSIS + Find database/s on multiple servers that match criteria you input + + .DESCRIPTION + Allows you to search SQL Server instances for database that have either the same name, owner or service broker guid. + + There a several reasons for the service broker guid not matching on a restored database primarily using alter database new broker. or turn off broker to return a guid of 0000-0000-0000-0000. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Property + What you would like to search on. Either Database Name, Owner, or Service Broker GUID. Database name is the default. + + .PARAMETER Pattern + Value that is searched for. This is a regular expression match but you can just use a plain ol string like 'dbareports' + + .PARAMETER Exact + Search for an exact match instead of a pattern + + .PARAMETER Detailed + Output all properties, will be depreciated in 1.0.0 release. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Author: Stephen Bennett: https://sqlnotesfromtheunderground.wordpress.com/ + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaDatabase + + .EXAMPLE + Find-DbaDatabase -SqlInstance "DEV01", "DEV02", "UAT01", "UAT02", "PROD01", "PROD02" -Pattern Report + + Returns all database from the SqlInstances that have a database with Report in the name + + .EXAMPLE + Find-DbaDatabase -SqlInstance "DEV01", "DEV02", "UAT01", "UAT02", "PROD01", "PROD02" -Pattern TestDB -Exact | Select-Object * + + Returns all database from the SqlInstances that have a database named TestDB with a detailed output. + + .EXAMPLE + Find-DbaDatabase -SqlInstance "DEV01", "DEV02", "UAT01", "UAT02", "PROD01", "PROD02" -Property ServiceBrokerGuid -Pattern '-faeb-495a-9898-f25a782835f5' | Select-Object * + + Returns all database from the SqlInstances that have the same Service Broker GUID with a deatiled output + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateSet('Name', 'ServiceBrokerGuid', 'Owner')] + [string]$Property = 'Name', + [parameter(Mandatory = $true)] + [string]$Pattern, + [switch]$Exact, + [switch]$Detailed, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Parameter Detailed + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($exact -eq $true) { + $dbs = $server.Databases | Where-Object IsAccessible | Where-Object { $_.$property -eq $pattern } + } + else { + try { + $dbs = $server.Databases | Where-Object IsAccessible | Where-Object { $_.$property.ToString() -match $pattern } + } + catch { + # they probably put asterisks thinking it's a like + $Pattern = $Pattern -replace '\*', '' + $Pattern = $Pattern -replace '\%', '' + $dbs = $server.Databases | Where-Object { $_.$property.ToString() -match $pattern } + } + } + + foreach ($db in $dbs) { + + $extendedproperties = @() + foreach ($xp in $db.ExtendedProperties) { + $extendedproperties += [PSCustomObject]@{ + Name = $db.ExtendedProperties[$xp.Name].Name + Value = $db.ExtendedProperties[$xp.Name].Value + } + } + + if ($extendedproperties.count -eq 0) { $extendedproperties = 0 } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.Name + Name = $db.Name + SizeMB = $db.Size + Owner = $db.Owner + CreateDate = $db.CreateDate + ServiceBrokerGuid = $db.ServiceBrokerGuid + Tables = ($db.Tables | Where-Object { $_.IsSystemObject -eq $false }).Count + StoredProcedures = ($db.StoredProcedures | Where-Object { $_.IsSystemObject -eq $false }).Count + Views = ($db.Views | Where-Object { $_.IsSystemObject -eq $false }).Count + ExtendedProperties = $extendedproperties + Database = $db + } | Select-DefaultView -ExcludeProperty Database, ExtendedProperties, ServiceBrokerGuid, StoredProcedures, Tables, Views + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Find-DbaDbGrowthEvent { + <# + .SYNOPSIS + Finds any database AutoGrow events in the Default Trace. + + .DESCRIPTION + Finds any database AutoGrow events in the Default Trace. + + The following events are included: + 92 - Data File Auto Grow + 93 - Log File Auto Grow + 94 - Data File Auto Shrink + 95 - Log File Auto Shrink + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER EventType + Provide a filter on growth event type to filter the results. + + Allowed values: Growth, Shrink + + .PARAMETER FileType + Provide a filter on file type to filter the results. + + Allowed vaules: Data, Log + + .PARAMETER UseLocalTime + Return the local time of the instance instead of converting to UTC. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: AutoGrow,Growth,Database + Author: Aaron Nelson + Query Extracted from SQL Server Management Studio (SSMS) 2016. + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaDatabaseGrowthEvent + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance localhost + + Returns any database AutoGrow events in the Default Trace with UTC time for the instance for every database on the localhost instance. + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance localhost -UseLocalTime + + Returns any database AutoGrow events in the Default Trace with the local time of the instance for every database on the localhost instance. + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance ServerA\SQL2016, ServerA\SQL2014 + + Returns any database AutoGrow events in the Default Traces for every database on ServerA\sql2016 & ServerA\SQL2014. + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance ServerA\SQL2016 | Format-Table -AutoSize -Wrap + + Returns any database AutoGrow events in the Default Trace for every database on the ServerA\SQL2016 instance in a table format. + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance ServerA\SQL2016 -EventType Shrink + + Returns any database Auto Shrink events in the Default Trace for every database on the ServerA\SQL2016 instance. + + .EXAMPLE + Find-DbaDatabaseGrowthEvent -SqlInstance ServerA\SQL2016 -EventType Growth -FileType Data + + Returns any database Auto Growth events on data files in the Default Trace for every database on the ServerA\SQL2016 instance. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [ValidateSet('Growth', 'Shrink')] + [string]$EventType, + [ValidateSet('Data', 'Log')] + [string]$FileType, + [switch]$UseLocalTime, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $eventClass = New-Object System.Collections.ArrayList + 92..95 | ForEach-Object { $null = $eventClass.Add($_) } + + if (Test-Bound 'EventType', 'FileType') { + switch ($FileType) { + 'Data' { + <# should only contain events for data: 92 (grow), 94 (shrink) #> + $eventClass.Remove(93) + $eventClass.Remove(95) + } + 'Log' { + <# should only contain events for log: 93 (grow), 95 (shrink) #> + $eventClass.Remove(92) + $eventClass.Remove(94) + } + } + switch ($EventType) { + 'Growth' { + <# should only contain events for growth: 92 (data), 93 (log) #> + $eventClass.Remove(94) + $eventClass.Remove(95) + } + 'Shrink' { + <# should only contain events for shrink: 94 (data), 95 (log) #> + $eventClass.Remove(92) + $eventClass.Remove(93) + } + } + } + + $eventClassFilter = $eventClass -join "," + + $sql = " + BEGIN TRY + IF (SELECT CONVERT(INT,[value_in_use]) FROM sys.configurations WHERE [name] = 'default trace enabled' ) = 1 + BEGIN + DECLARE @curr_tracefilename VARCHAR(500); + DECLARE @base_tracefilename VARCHAR(500); + DECLARE @indx INT; + + SELECT @curr_tracefilename = [path] + FROM sys.traces + WHERE is_default = 1 ; + + SET @curr_tracefilename = REVERSE(@curr_tracefilename); + SELECT @indx = PATINDEX('%\%', @curr_tracefilename); + SET @curr_tracefilename = REVERSE(@curr_tracefilename); + SET @base_tracefilename = LEFT( @curr_tracefilename,LEN(@curr_tracefilename) - @indx) + '\log.trc'; + + SELECT + SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + CONVERT(INT,(DENSE_RANK() OVER (ORDER BY [StartTime] DESC))%2) AS OrderRank, + CONVERT(INT, [EventClass]) AS EventClass, + [DatabaseName], + [Filename], + CONVERT(INT,(Duration/1000)) AS Duration, + $(if (-not $UseLocalTime) { " + DATEADD (MINUTE, DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()), [StartTime]) AS StartTime, -- Convert to UTC time + DATEADD (MINUTE, DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()), [EndTime]) AS EndTime, -- Convert to UTC time" + } + else { " + [StartTime] AS StartTime, + [EndTime] AS EndTime," + }) + ([IntegerData]*8.0/1024) AS ChangeInSize, + ApplicationName, + HostName, + SessionLoginName, + SPID + FROM::fn_trace_gettable( @base_tracefilename, DEFAULT ) + WHERE + [EventClass] IN ($eventClassFilter) + AND [ServerName] = @@SERVERNAME + AND [DatabaseName] IN (_DatabaseList_) + ORDER BY [StartTime] DESC; + END + ELSE + SELECT + SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + -100 AS [OrderRank], + -1 AS [OrderRank], + 0 AS [EventClass], + 0 [DatabaseName], + 0 AS [Filename], + 0 AS [Duration], + 0 AS [StartTime], + 0 AS [EndTime], + 0 AS ChangeInSize, + 0 AS [ApplicationName], + 0 AS [HostName], + 0 AS [SessionLoginName], + 0 AS [SPID] + END TRY + BEGIN CATCH + SELECT + SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + -100 AS [OrderRank], + -100 AS [OrderRank], + ERROR_NUMBER() AS [EventClass], + ERROR_SEVERITY() AS [DatabaseName], + ERROR_STATE() AS [Filename], + ERROR_MESSAGE() AS [Duration], + 1 AS [StartTime], + 1 AS [EndTime], + 1 AS [ChangeInSize], + 1 AS [ApplicationName], + 1 AS [HostName], + 1 AS [SessionLoginName], + 1 AS [SPID] + END CATCH" + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Find-DbaDatabaseGrowthEvent + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + #Create dblist name in 'bd1', 'db2' format + $dbsList = "'$($($dbs | ForEach-Object {$_.Name}) -join "','")'" + Write-Message -Level Verbose -Message "Executing query against $dbsList on $instance" + + $sql = $sql -replace '_DatabaseList_', $dbsList + Write-Message -Level Debug -Message "Executing SQL Statement:`n $sql" + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'EventClass', 'DatabaseName', 'Filename', 'Duration', 'StartTime', 'EndTime', 'ChangeInSize', 'ApplicationName', 'HostName' + + try { + Select-DefaultView -InputObject $server.Query($sql) -Property $defaults + } + catch { + Stop-Function -Message "Issue collecting data on $server" -Target $server -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + } +} +function Find-DbaDisabledIndex { + <# + .SYNOPSIS + Find Disabled indexes + + .DESCRIPTION + This command will help you to find disabled indexes on a database or a list of databases. + + .PARAMETER SqlInstance + The SQL Server you want to check for disabled indexes. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER NoClobber + If this switch is enabled, the output file will not be overwritten. + + .PARAMETER Append + If this switch is enabled, content will be appended to the output file. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Index + Author: Jason Squires, sqlnotnull.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbadisabledIndex + + .EXAMPLE + Find-DbadisabledIndex -SqlInstance sql2005 + + Generates the SQL statements to drop the selected disabled indexes on server "sql2005". + + .EXAMPLE + Find-DbadisabledIndex -SqlInstance sqlserver2016 -SqlCredential $cred + + Generates the SQL statements to drop the selected disabled indexes on server "sqlserver2016", using SQL Authentication to connect to the database. + + .EXAMPLE + Find-DbadisabledIndex -SqlInstance sqlserver2016 -Database db1, db2 + + Generates the SQL Statement to drop selected indexes in databases db1 & db2 on server "sqlserver2016". + + .EXAMPLE + Find-DbadisabledIndex -SqlInstance sqlserver2016 + + Generates the SQL statements to drop selected indexes on all user databases. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$NoClobber, + [switch]$Append, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = " + SELECT DB_NAME() AS 'DatabaseName' + ,s.name AS 'SchemaName' + ,t.name AS 'TableName' + ,i.object_id AS ObjectId + ,i.name AS 'IndexName' + ,i.index_id as 'IndexId' + ,i.type_desc as 'TypeDesc' + FROM sys.tables t + JOIN sys.schemas s + ON t.schema_id = s.schema_id + JOIN sys.indexes i + ON i.object_id = t.object_id + WHERE i.is_disabled = 1" + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $databases = $server.Databases | Where-Object Name -in $database + } + else { + $databases = $server.Databases | Where-Object IsAccessible -eq $true + } + + if ($databases.Count -gt 0) { + foreach ($db in $databases.name) { + + if ($ExcludeDatabase -contains $db -or $null -eq $server.Databases[$db]) { + continue + } + + try { + if ($PSCmdlet.ShouldProcess($db, "Getting disabled indexes")) { + Write-Message -Level Verbose -Message "Getting indexes from database '$db'." + Write-Message -Level Debug -Message "SQL Statement: $sql" + $disabledIndex = $server.Databases[$db].ExecuteWithResults($sql) + + if ($disabledIndex.Tables[0].Rows.Count -gt 0) { + $results = $disabledIndex.Tables[0]; + if ($results.Count -gt 0 -or !([string]::IsNullOrEmpty($results))) { + foreach ($index in $results) { + $index + } + } + } + else { + Write-Message -Level Verbose -Message "No Disabled indexes found!" + } + } + } + catch { + Stop-Function -Message "Issue gathering indexes" -Category InvalidOperation -InnerErrorRecord $_ -Target $db + } + } + } + else { + Write-Message -Level Verbose -Message "There are no databases to analyse." + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlDisabledIndex + } +} +function Find-DbaDuplicateIndex { + <# + .SYNOPSIS + Find duplicate and overlapping indexes. + + .DESCRIPTION + This command will help you to find duplicate and overlapping indexes on a database or a list of databases. + + On SQL Server 2008 and higher, the IsFiltered property will also be checked + + Also tells how much space you can save by dropping the index. + + We show the type of compression so you can make a more considered decision. + + For now only supports CLUSTERED and NONCLUSTERED indexes. + + You can select the indexes you want to drop on the gridview and when clicking OK, the DROP statement will be generated. + + Output: + TableName + IndexName + KeyColumns + IncludedColumns + IndexSizeMB + IndexType + CompressionDescription (When 2008+) + [RowCount] + IsDisabled + IsFiltered (When 2008+) + + .PARAMETER SqlInstance + The SQL Server you want to check for duplicate indexes. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER IncludeOverlapping + If this switch is enabled, indexes which are partially duplicated will be returned. + + Example: If the first key column is the same between two indexes, but one has included columns and the other not, this will be shown. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the DROP statement(s) will be executed instead of being written to the output file. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Index + Author: Claudio Silva (@ClaudioESSilva) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaDuplicateIndex + + .EXAMPLE + Find-DbaDuplicateIndex -SqlInstance sql2005 | Out-File -FilePath C:\temp\sql2005-DuplicateIndexes.sql + + Generates SQL statements to drop the selected duplicate indexes in server "sql2005" and writes them to the file "C:\temp\sql2005-DuplicateIndexes.sql" + + .EXAMPLE + Find-DbaDuplicateIndex -SqlInstance sql2005 | Out-File -FilePath C:\temp\sql2005-DuplicateIndexes.sql -Append + + Generates SQL statements to drop the selected duplicate indexes and writes/appends them to the file "C:\temp\sql2005-DuplicateIndexes.sql" + + .EXAMPLE + Find-DbaDuplicateIndex -SqlInstance sqlserver2014a -SqlCredential $cred + + Finds exact duplicate indexes on all user databases present on sqlserver2014a, using SQL authentication. + + .EXAMPLE + Find-DbaDuplicateIndex -SqlInstance sqlserver2014a -Database db1, db2 + + Finds exact duplicate indexes on the db1 and db2 databases. + + .EXAMPLE + Find-DbaDuplicateIndex -SqlInstance sqlserver2014a -IncludeOverlapping + + Finds both duplicate and overlapping indexes on all user databases. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [switch]$IncludeOverlapping, + [switch]$EnableException + ) + + begin { + $exactDuplicateQuery2005 = " + WITH CTE_IndexCols + AS ( + SELECT i.[object_id] + ,i.index_id + ,OBJECT_SCHEMA_NAME(i.[object_id]) AS SchemaName + ,OBJECT_NAME(i.[object_id]) AS TableName + ,NAME AS IndexName + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 0 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS KeyColumns + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 1 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS IncludedColumns + ,i.[type_desc] AS IndexType + ,i.is_disabled AS IsDisabled + FROM sys.indexes AS i + WHERE i.index_id > 0 -- Exclude HEAPS + AND i.[type_desc] IN ( + 'CLUSTERED' + ,'NONCLUSTERED' + ) + AND OBJECT_SCHEMA_NAME(i.[object_id]) <> 'sys' + ) + ,CTE_IndexSpace + AS ( + SELECT s.[object_id] + ,s.index_id + ,SUM(s.[used_page_count]) * 8 / 1024.0 AS IndexSizeMB + ,SUM(p.[rows]) AS [RowCount] + FROM sys.dm_db_partition_stats AS s + INNER JOIN sys.partitions p WITH (NOLOCK) ON s.[partition_id] = p.[partition_id] + AND s.[object_id] = p.[object_id] + AND s.index_id = p.index_id + WHERE s.index_id > 0 -- Exclude HEAPS + AND OBJECT_SCHEMA_NAME(s.[object_id]) <> 'sys' + GROUP BY s.[object_id] + ,s.index_id + ) + SELECT DB_NAME() AS DatabaseName + ,CI1.SchemaName + '.' + CI1.TableName AS 'TableName' + ,CI1.IndexName + ,CI1.KeyColumns + ,CI1.IncludedColumns + ,CI1.IndexType + ,CSPC.IndexSizeMB + ,CSPC.[RowCount] + ,CI1.IsDisabled + FROM CTE_IndexCols AS CI1 + INNER JOIN CTE_IndexSpace AS CSPC ON CI1.[object_id] = CSPC.[object_id] + AND CI1.index_id = CSPC.index_id + WHERE EXISTS ( + SELECT 1 + FROM CTE_IndexCols CI2 + WHERE CI1.SchemaName = CI2.SchemaName + AND CI1.TableName = CI2.TableName + AND CI1.KeyColumns = CI2.KeyColumns + AND CI1.IncludedColumns = CI2.IncludedColumns + AND CI1.IndexName <> CI2.IndexName + )" + + $overlappingQuery2005 = " + WITH CTE_IndexCols + AS ( + SELECT i.[object_id] + ,i.index_id + ,OBJECT_SCHEMA_NAME(i.[object_id]) AS SchemaName + ,OBJECT_NAME(i.[object_id]) AS TableName + ,NAME AS IndexName + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 0 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS KeyColumns + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 1 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS IncludedColumns + ,i.[type_desc] AS IndexType + ,i.is_disabled AS IsDisabled + FROM sys.indexes AS i + WHERE i.index_id > 0 -- Exclude HEAPS + AND i.[type_desc] IN ( + 'CLUSTERED' + ,'NONCLUSTERED' + ) + AND OBJECT_SCHEMA_NAME(i.[object_id]) <> 'sys' + ) + ,CTE_IndexSpace + AS ( + SELECT s.[object_id] + ,s.index_id + ,SUM(s.[used_page_count]) * 8 / 1024.0 AS IndexSizeMB + ,SUM(p.[rows]) AS [RowCount] + FROM sys.dm_db_partition_stats AS s + INNER JOIN sys.partitions p WITH (NOLOCK) ON s.[partition_id] = p.[partition_id] + AND s.[object_id] = p.[object_id] + AND s.index_id = p.index_id + WHERE s.index_id > 0 -- Exclude HEAPS + AND OBJECT_SCHEMA_NAME(s.[object_id]) <> 'sys' + GROUP BY s.[object_id] + ,s.index_id + ) + SELECT DB_NAME() AS DatabaseName + ,CI1.SchemaName + '.' + CI1.TableName AS 'TableName' + ,CI1.IndexName + ,CI1.KeyColumns + ,CI1.IncludedColumns + ,CI1.IndexType + ,CSPC.IndexSizeMB + ,CSPC.[RowCount] + ,CI1.IsDisabled + FROM CTE_IndexCols AS CI1 + INNER JOIN CTE_IndexSpace AS CSPC ON CI1.[object_id] = CSPC.[object_id] + AND CI1.index_id = CSPC.index_id + WHERE EXISTS ( + SELECT 1 + FROM CTE_IndexCols CI2 + WHERE CI1.SchemaName = CI2.SchemaName + AND CI1.TableName = CI2.TableName + AND ( + ( + CI1.KeyColumns LIKE CI2.KeyColumns + '%' + AND SUBSTRING(CI1.KeyColumns, LEN(CI2.KeyColumns) + 1, 1) = ' ' + ) + OR ( + CI2.KeyColumns LIKE CI1.KeyColumns + '%' + AND SUBSTRING(CI2.KeyColumns, LEN(CI1.KeyColumns) + 1, 1) = ' ' + ) + ) + AND CI1.IndexName <> CI2.IndexName + )" + + # Support Compression 2008+ + $exactDuplicateQuery = " + WITH CTE_IndexCols + AS ( + SELECT i.[object_id] + ,i.index_id + ,OBJECT_SCHEMA_NAME(i.[object_id]) AS SchemaName + ,OBJECT_NAME(i.[object_id]) AS TableName + ,NAME AS IndexName + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 0 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS KeyColumns + ,ISNULL(STUFF(( + SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 + THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 1 + ORDER BY idxCol.key_ordinal + FOR XML PATH('') + ), 1, 2, ''), '') AS IncludedColumns + ,i.[type_desc] AS IndexType + ,i.is_disabled AS IsDisabled + ,i.has_filter AS IsFiltered + FROM sys.indexes AS i + WHERE i.index_id > 0 -- Exclude HEAPS + AND i.[type_desc] IN ( + 'CLUSTERED' + ,'NONCLUSTERED' + ) + AND OBJECT_SCHEMA_NAME(i.[object_id]) <> 'sys' + ) + ,CTE_IndexSpace + AS ( + SELECT s.[object_id] + ,s.index_id + ,SUM(s.[used_page_count]) * 8 / 1024.0 AS IndexSizeMB + ,SUM(p.[rows]) AS [RowCount] + ,p.data_compression_desc AS CompressionDescription + FROM sys.dm_db_partition_stats AS s + INNER JOIN sys.partitions p WITH (NOLOCK) ON s.[partition_id] = p.[partition_id] + AND s.[object_id] = p.[object_id] + AND s.index_id = p.index_id + WHERE s.index_id > 0 -- Exclude HEAPS + AND OBJECT_SCHEMA_NAME(s.[object_id]) <> 'sys' + GROUP BY s.[object_id] + ,s.index_id + ,p.data_compression_desc + ) + SELECT DB_NAME() AS DatabaseName + ,CI1.SchemaName + '.' + CI1.TableName AS 'TableName' + ,CI1.IndexName + ,CI1.KeyColumns + ,CI1.IncludedColumns + ,CI1.IndexType + ,CSPC.IndexSizeMB + ,CSPC.CompressionDescription + ,CSPC.[RowCount] + ,CI1.IsDisabled + ,CI1.IsFiltered + FROM CTE_IndexCols AS CI1 + INNER JOIN CTE_IndexSpace AS CSPC ON CI1.[object_id] = CSPC.[object_id] + AND CI1.index_id = CSPC.index_id + WHERE EXISTS ( + SELECT 1 + FROM CTE_IndexCols CI2 + WHERE CI1.SchemaName = CI2.SchemaName + AND CI1.TableName = CI2.TableName + AND CI1.KeyColumns = CI2.KeyColumns + AND CI1.IncludedColumns = CI2.IncludedColumns + AND CI1.IsFiltered = CI2.IsFiltered + AND CI1.IndexName <> CI2.IndexName + )" + + $overlappingQuery = " + WITH CTE_IndexCols AS + ( + SELECT + i.[object_id] + ,i.index_id + ,OBJECT_SCHEMA_NAME(i.[object_id]) AS SchemaName + ,OBJECT_NAME(i.[object_id]) AS TableName + ,Name AS IndexName + ,ISNULL(STUFF((SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col + ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 0 + ORDER BY idxCol.key_ordinal + FOR XML PATH('')), 1, 2, ''), '') AS KeyColumns + ,ISNULL(STUFF((SELECT ', ' + col.NAME + ' ' + CASE + WHEN idxCol.is_descending_key = 1 THEN 'DESC' + ELSE 'ASC' + END -- Include column order (ASC / DESC) + FROM sys.index_columns idxCol + INNER JOIN sys.columns col + ON idxCol.[object_id] = col.[object_id] + AND idxCol.column_id = col.column_id + WHERE i.[object_id] = idxCol.[object_id] + AND i.index_id = idxCol.index_id + AND idxCol.is_included_column = 1 + ORDER BY idxCol.key_ordinal + FOR XML PATH('')), 1, 2, ''), '') AS IncludedColumns + ,i.[type_desc] AS IndexType + ,i.is_disabled AS IsDisabled + ,i.has_filter AS IsFiltered + FROM sys.indexes AS i + WHERE i.index_id > 0 -- Exclude HEAPS + AND i.[type_desc] IN ('CLUSTERED', 'NONCLUSTERED') + AND OBJECT_SCHEMA_NAME(i.[object_id]) <> 'sys' + ), + CTE_IndexSpace AS + ( + SELECT + s.[object_id] + ,s.index_id + ,SUM(s.[used_page_count]) * 8 / 1024.0 AS IndexSizeMB + ,SUM(p.[rows]) AS [RowCount] + ,p.data_compression_desc AS CompressionDescription + FROM sys.dm_db_partition_stats AS s + INNER JOIN sys.partitions p WITH (NOLOCK) + ON s.[partition_id] = p.[partition_id] + AND s.[object_id] = p.[object_id] + AND s.index_id = p.index_id + WHERE s.index_id > 0 -- Exclude HEAPS + AND OBJECT_SCHEMA_NAME(s.[object_id]) <> 'sys' + GROUP BY s.[object_id], s.index_id, p.data_compression_desc + ) + SELECT + DB_NAME() AS DatabaseName + ,CI1.SchemaName + '.' + CI1.TableName AS 'TableName' + ,CI1.IndexName + ,CI1.KeyColumns + ,CI1.IncludedColumns + ,CI1.IndexType + ,CSPC.IndexSizeMB + ,CSPC.CompressionDescription + ,CSPC.[RowCount] + ,CI1.IsDisabled + ,CI1.IsFiltered + FROM CTE_IndexCols AS CI1 + INNER JOIN CTE_IndexSpace AS CSPC + ON CI1.[object_id] = CSPC.[object_id] + AND CI1.index_id = CSPC.index_id + WHERE EXISTS (SELECT 1 + FROM CTE_IndexCols CI2 + WHERE CI1.SchemaName = CI2.SchemaName + AND CI1.TableName = CI2.TableName + AND ( + (CI1.KeyColumns like CI2.KeyColumns + '%' and SUBSTRING(CI1.KeyColumns,LEN(CI2.KeyColumns)+1,1) = ' ') + OR (CI2.KeyColumns like CI1.KeyColumns + '%' and SUBSTRING(CI2.KeyColumns,LEN(CI1.KeyColumns)+1,1) = ' ') + ) + AND CI1.IsFiltered = CI2.IsFiltered + AND CI1.IndexName <> CI2.IndexName + )" + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($database) { + $databases = $server.Databases | Where-Object Name -in $database + } + else { + $databases = $server.Databases | Where-Object IsAccessible -eq $true + } + + foreach ($db in $databases) { + try { + Write-Message -Level Verbose -Message "Getting indexes from database '$db'." + + $query = if ($server.versionMajor -eq 9) { + if ($IncludeOverlapping) { $overlappingQuery2005 } + else { $exactDuplicateQuery2005 } + } + else { + if ($IncludeOverlapping) { $overlappingQuery } + else { $exactDuplicateQuery } + } + + $db.Query($query) + + } + catch { + Stop-Function -Message "Query failure" -Target $db + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlDuplicateIndex + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Find-DbaInstance { + <# + .SYNOPSIS + Search for SQL Server Instances. + + .DESCRIPTION + This function searches for SQL Server Instances. + + It supports a variety of scans for this purpose which can be separated in two categories: + - Discovery + - Scan + + Discovery: + This is where it compiles a list of computers / addresses to check. + It supports several methods of generating such lists (including Active Directory lookup or IP Ranges), but also supports specifying a list of computers to check. + - For details on discovery, see the documentation on the '-DiscoveryType' parameter + - For details on explicitly providing a list, see the documentation on the '-ComputerName' parameter + + Scan: + Once a list of computers has been provided, this command will execute a variety of actions to determine any instances present for each of them. + This is described in more detail in the documentation on the '-ScanType' parameter. + Additional parameters allow more granular control over individual scans (e.g. Credentials to use). + + Note on logging and auditing: + The Discovery phase is unproblematic since it is non-intrusive, however during the scan phase, all targeted computers may be accessed repeatedly. + This may cause issues with security teams, due to many logon events and possibly failed authentication. + This action constitutes a network scan, which may be illegal depending on the nation you are in and whether you own the network you scan. + If you are unsure whether you may use this command in your environment, check the detailed description on the '-ScanType' parameter and contact your IT security team for advice. + + .PARAMETER ComputerName + The computer to scan. Can be a variety of input types, including text or the output of Get-ADComputer. + Any extra instance information (such as connection strings or live sql server connections) beyond the computername will be discarded. + + .PARAMETER DiscoveryType + The mechanisms to be used to discover instances. + Supports any combination of: + - Service Principal Name lookup ('Domain'; from Active Directory) + - SQL Instance Enumeration ('DataSourceEnumeration'; same as SSMS uses) + - IP Address range ('IPRange'; all IP Addresses will be scanned) + + SPN Lookup: + The function tries to connect active directory to look up all computers with registered SQL Instances. + Not all instances need to be registered properly, making this not 100% reliable. + By default, your nearest Domain Controller is contacted for this scan. + However it is possible to explicitly state the DC to contact using its DistinguishedName and the '-DomainController' parameter. + If credentials were specified using the '-Credential' parameter, those same credentials are used to perform this lookup, allowing the scan of other domains. + + SQL Instance Enumeration: + This uses the default UDP Broadcast based instance enumeration used by SSMS to detect instances. + Note that the result from this is not used in the actual scan, but only to compile a list of computers to scan. + To enable the same results for the scan, ensure that the 'Browser' scan is enabled. + + IP Address range: + This 'Discovery' uses a range of IPAddresses and simply passes them on to be tested. + See the 'Description' part of help on security issues of network scanning. + By default, it will enumerate all ethernet network adapters on the local computer and scan the entire subnet they are on. + By using the '-IpAddress' parameter, custom network ranges can be specified. + + .PARAMETER Credential + The credentials to use on windows network connection. + These credentials are used for: + - Contact to domain controllers for SPN lookups (only if explicit Domain Controller is specified) + - CIM/WMI contact to the scanned computers during the scan phase (see the '-ScanType' parameter documentation on affected scans). + + .PARAMETER SqlCredential + The credentials used to connect to SqlInstances to during the scan phase. + See the '-ScanType' parameter documentation on affected scans. + + .PARAMETER ScanType + The scans are the individual methods used to retrieve information about the scanned computer and any potentially installed instances. + This parameter is optional, by default all scans except for establishing an actual SQL connection are performed. + Scans can be specified in any arbitrary combination, however at least one instance detecting scan needs to be specified in order for data to be returned. + + Scans: + DNSResolve + - Tries resolving the computername in DNS + Ping + - Tries pinging the computer. Failure will NOT terminate scans. + SQLService + - Tries listing all SQL Services using CIM/WMI + - This scan uses credentials specified in the '-Credential' parameter if any. + - This scan detects instances. + - Success in this scan guarantees high confidence (See parameter '-MinimumConfidence' for details). + Browser + - Tries discovering all instances via the browser service + - This scan detects instances. + TCPPort + - Tries connecting to the TCP Ports. + - By default, port 1433 is connected to. + - The parameter '-TCPPort' can be used to provide a list of port numbers to scan. + - This scan detects possible instances. Since other services might bind to a given port, this is not the most reliable test. + - This scan is also used to validate found SPNs if both scans are used in combination + SqlConnect + - Tries to establish a SQL connection to the server + - Uses windows credentials by default + - Specify custom credentials using the '-SqlCredential' parameter + - This scan is not used by default + - Success in this scan guarantees high confidence (See parameter '-MinimumConfidence' for details). + SPN + - Tries looking up the Service Principal Names for each instance + - Will use the nearest Domain Controller by default + - Target a specific domain controller using the '-DomainController' parameter + - If using the '-DomainController' parameter, use the '-Credential' parameter to specify the credentials used to connect + + .PARAMETER IpAddress + This parameter can be used to override the defaults for the IPRange discovery. + This parameter accepts a list of strings supporting any combination of: + - Plain IP Addresses (e.g.: "10.1.1.1") + - IP Address Ranges (e.g.: "10.1.1.1-10.1.1.5") + - IP Address & Subnet Mask (e.g.: "10.1.1.1/255.255.255.0") + - IP Address & Subnet Length: (e.g.: "10.1.1.1/24) + Overlapping addresses will not result in duplicate scans. + + .PARAMETER DomainController + The domain controller to contact for SPN lookups / searches. + Uses the credentials from the '-Credential' parameter if specified. + + .PARAMETER TCPPort + The ports to scan in the TCP Port Scan method. + Defaults to 1433. + + .PARAMETER MinimumConfidence + This command tries to discover instances, which isn't always a sure thing. + Depending on the number and type of scans completed, we have different levels of confidence in our results. + By default, we will return anything that we have at least a low confidence of being an instance. + These are the confidence levels we support and how they are determined: + - High: Established SQL Connection (including rejection for bad credentials) or service scan. + - Medium: Browser reply or a combination of TCPConnect _and_ SPN test. + - Low: Either TCPConnect _or_ SPN + - None: Computer existence could be verified, but no sign of an SQL Instance + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, Connect, SqlServer + Author: Scott Sutherland, 2018 NetSPI + + Conversion & Refactoring by: Friedrich Weinmann + Outside resources used and modified: + https://gallery.technet.microsoft.com/scriptcenter/List-the-IP-addresses-in-a-60c5bb6b + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaInstance + + .EXAMPLE + PS C:\> Find-DbaInstance -DiscoveryType Domain,DataSourceEnumeration + + Performs a network search for SQL Instances by: + - Looking up the Service Principal Names of computers in active directory + - Using the UDP broadcast based auto-discovery of SSMS + After that it will extensively scan all hosts thus discovered for instances. + + .EXAMPLE + PS C:\> Find-DbaInstance -DiscoveryType All + + Performs a network search for SQL Instances, using all discovery protocols: + - Active directory search for Service Principal Names + - SQL Instance Enumeration (same as SSMS does) + - All IPAddresses in the current computer's subnets of all connected network interfaces + Note: This scan will take a long time, due to including the IP Scan + + .EXAMPLE + PS C:\> Get-ADComputer -Filter "*" | Find-DbaInstance + + Scans all computers in the domain for SQL Instances, using a deep probe: + - Tries resolving the name in DNS + - Tries pinging the computer + - Tries listing all SQL Services using CIM/WMI + - Tries discovering all instances via the browser service + - Tries connecting to the default TCP Port (1433) + - Tries connecting to the TCP port of each discovered instance + - Tries to establish a SQL connection to the server using default windows credentials + - Tries looking up the Service Principal Names for each instance + + .EXAMPLE + PS C:\> Get-Content .\servers.txt | Find-DbaInstance -SqlCredential $cred -ScanType Browser,SqlConnect + + Reads all servers from the servers.txt file (one server per line), + then scans each of them for instances using the browser service + and finally attempts to connect to each instance found using the specified credentials. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Computer', ValueFromPipeline = $true)] + [DbaInstance[]]$ComputerName, + [Parameter(Mandatory = $true, ParameterSetName = 'Discover')] + [Sqlcollaborative.Dbatools.Discovery.DbaInstanceDiscoveryType]$DiscoveryType, + [System.Management.Automation.PSCredential]$Credential, + [System.Management.Automation.PSCredential]$SqlCredential, + [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]$ScanType = "Default", + [Parameter(ParameterSetName = 'Discover')] + [string[]]$IpAddress, + [string]$DomainController, + [int[]]$TCPPort = 1433, + [Sqlcollaborative.Dbatools.Discovery.DbaInstanceConfidenceLevel]$MinimumConfidence = 'Low', + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # TCPPort = 1 | SqlService = 4 | SPN = 16 | Browser = 32 + if (-not ($ScanType -band 53)) { + Stop-Function -Message "Invalid Scan Types specified: $ScanType | Specify at least one of the following types: Browser, SqlService, SPN, TCPPort, Default or All. Otherwise no detection will be possible." -EnableException $EnableException -Category InvalidArgument + return + } + + #region Utility Functions + function Test-SqlInstance { + <# + .SYNOPSIS + Performs the actual scanning logic + + .DESCRIPTION + Performs the actual scanning logic + Each potential target is accessed using the specified scan routines. + + .PARAMETER Target + The target to scan. + + .EXAMPLE + PS C:\> Test-SqlInstance + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)][DbaInstance[]]$Target, + [PSCredential]$Credential, + [PSCredential]$SqlCredential, + [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]$ScanType, + [string]$DomainController, + [int[]]$TCPPort = 1433, + [Sqlcollaborative.Dbatools.Discovery.DbaInstanceConfidenceLevel]$MinimumConfidence, + [switch]$EnableException + ) + + begin { + [System.Collections.ArrayList]$computersScanned = @() + } + + process { + foreach ($computer in $Target) { + if ($computersScanned.Contains($computer.ComputerName)) { + continue + } + else { + $null = $computersScanned.Add($computer.ComputerName) + } + Write-Message -Level Verbose -Message "Processing: $($computer)" -Target $computer -EnableException $EnableException -FunctionName Find-DbaInstance + + #region Null variables to prevent scope lookup on conditional existence + $resolution = $null + $pingReply = $null + $sPNs = @() + $ports = @() + $browseResult = $null + $services = @() + $serverObject = $null + $browseFailed = $false + #endregion Null variables to prevent scope lookup on conditional existence + + #region Gather data + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::DNSResolve) { + try { $resolution = [System.Net.Dns]::GetHostEntry($computer.ComputerName) } + catch { } + } + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::Ping) { + $ping = New-Object System.Net.NetworkInformation.Ping + try { $pingReply = $ping.Send($computer.ComputerName) } + catch { } + } + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::SPN) { + $computerByName = $computer.ComputerName + if ($resolution.HostName) { $computerByName = $resolution.HostName } + if ($computerByName -notmatch "$([dbargx]::IPv4)|$([dbargx]::IPv6)") { + try { $sPNs = Get-DomainSPN -DomainController $DomainController -Credential $Credential -ComputerName $computerByName -GetSPN } + catch { } + } + } + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::TCPPort) { + $ports = $TCPPort | Test-TcpPort -ComputerName $computer + } + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::Browser) { + try { + $browseResult = Get-SQLInstanceBrowserUDP -ComputerName $computer -EnableException + } + catch { + $browseFailed = $true + } + } + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::SqlService) { + if ($Credential) { $services = Get-DbaSqlService -ComputerName $computer -Credential $Credential -EnableException -ErrorAction Ignore -WarningAction SilentlyCOntinue } + else { $services = Get-DbaSqlService -ComputerName $computer -ErrorAction Ignore -WarningAction SilentlyContinue } + } + #endregion Gather data + + #region Gather list of found instance indicators + $instanceNames = @() + if ($Services) { + $Services | Select-Object -ExpandProperty InstanceName -Unique | Where-Object { $_ -and ($instanceNames -notcontains $_) } | ForEach-Object { + $instanceNames += $_ + } + } + if ($browseResult) { + $browseResult | Select-Object -ExpandProperty InstanceName -Unique | Where-Object { $_ -and ($instanceNames -notcontains $_) } | ForEach-Object { + $instanceNames += $_ + } + } + + $portsDetected = @() + foreach ($portResult in $ports) { + if ($portResult.IsOpen) { $portsDetected += $portResult.Port } + } + foreach ($sPN in $sPNs) { + try { $inst = $sPN.Split(':')[1] } + catch { continue } + + try { + [int]$portNumber = $inst + if ($portNumber -and ($portsDetected -notcontains $portNumber)) { + $portsDetected += $portNumber + } + } + catch { + if ($inst -and ($instanceNames -notcontains $inst)) { + $instanceNames += $inst + } + } + } + #endregion Gather list of found instance indicators + + #region Case: Nothing found + if ((-not $instanceNames) -and (-not $portsDetected)) { + if ($resolution -or ($pingReply.Status -like "Success")) { + if ($MinimumConfidence -eq [Sqlcollaborative.Dbatools.Discovery.DbaInstanceConfidenceLevel]::None) { + New-Object Sqlcollaborative.Dbatools.Discovery.DbaInstanceReport -Property @{ + MachineName = $computer.ComputerName + ComputerName = $computer.ComputerName + Ping = $pingReply.Status -like 'Success' + } + } + else { + Write-Message -Level Verbose -Message "Computer $computer could be contacted, but no trace of an SQL Instance was found. Skipping..." -Target $computer -EnableException $EnableException -FunctionName Find-DbaInstance + } + } + else { + Write-Message -Level Verbose -Message "Computer $computer could not be contacted, skipping." -Target $computer -EnableException $EnableException -FunctionName Find-DbaInstance + } + + continue + } + #endregion Case: Nothing found + + [System.Collections.ArrayList]$masterList = @() + + #region Case: Named instance found + foreach ($instance in $instanceNames) { + $object = New-Object Sqlcollaborative.Dbatools.Discovery.DbaInstanceReport + $object.MachineName = $computer.ComputerName + $object.ComputerName = $computer.ComputerName + $object.InstanceName = $instance + $object.DnsResolution = $resolution + $object.Ping = $pingReply.Status -like 'Success' + $object.ScanTypes = $ScanType + $object.Services = $services | Where-Object InstanceName -EQ $instance + $object.SystemServices = $services | Where-Object { -not $_.InstanceName } + $object.SPNs = $sPNs + + if ($result = $browseResult | Where-Object InstanceName -EQ $instance) { + $object.BrowseReply = $result + } + if ($ports) { + $object.PortsScanned = $ports + } + + if ($object.BrowseReply) { + $object.Confidence = 'Medium' + if ($object.BrowseReply.TCPPort) { + $object.Port = $object.BrowseReply.TCPPort + + $object.PortsScanned | Where-Object Port -EQ $object.Port | ForEach-Object { + $object.TcpConnected = $_.IsOpen + } + } + } + if ($object.Services) { + $object.Confidence = 'High' + + $engine = $object.Services | Where-Object ServiceType -EQ "Engine" + switch ($engine.State) { + "Running" { $object.Availability = 'Available' } + "Stopped" { $object.Availability = 'Unavailable' } + default { $object.Availability = 'Unknown' } + } + } + + $object.Timestamp = Get-Date + + $masterList += $object + } + #endregion Case: Named instance found + + #region Case: Port number found + foreach ($port in $portsDetected) { + if ($masterList.Port -contains $port) { continue } + + $object = New-Object Sqlcollaborative.Dbatools.Discovery.DbaInstanceReport + $object.MachineName = $computer.ComputerName + $object.ComputerName = $computer.ComputerName + $object.Port = $port + $object.DnsResolution = $resolution + $object.Ping = $pingReply.Status -like 'Success' + $object.ScanTypes = $ScanType + $object.SystemServices = $services | Where-Object { -not $_.InstanceName } + $object.SPNs = $sPNs + $object.Confidence = 'Low' + if ($ports) { + $object.PortsScanned = $ports + + if (($ports | Where-Object IsOpen).Port -eq 1433) { + $object.Confidence = 'Medium' + } + } + + if (($ports.Port -contains $port) -and ($sPNs | Where-Object { $_ -like "*:$port" })) { + $object.Confidence = 'Medium' + } + + $object.PortsScanned | Where-Object Port -EQ $object.Port | ForEach-Object { + $object.TcpConnected = $_.IsOpen + } + $object.Timestamp = Get-Date + + if ($masterList.SqlInstance -contains $object.SqlInstance) { + continue + } + + $masterList += $object + } + #endregion Case: Port number found + + if ($ScanType -band [Sqlcollaborative.Dbatools.Discovery.DbaInstanceScanType]::SqlConnect) { + $instanceHash = @{ } + $toDelete = @() + foreach ($dataSet in $masterList) { + try { + $server = Connect-SqlInstance -SqlInstance $dataSet.FullSmoName -SqlCredential $SqlCredential + $dataSet.SqlConnected = $true + $dataSet.Confidence = 'High' + + # Remove duplicates + if ($instanceHash.ContainsKey($server.DomainInstanceName)) { + $toDelete += $dataSet + } + else { + $instanceHash[$server.DomainInstanceName] = $dataSet + + try { + $dataSet.MachineName = $server.ComputerNamePhysicalNetBIOS + } + catch { } + } + } + catch { + # Error class definitions + # https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-error-severities + # 24 or less means an instance was found, but had some issues + + #region Processing error (Access denied, server error, ...) + if ($_.Exception.InnerException.Errors.Class -lt 25) { + # There IS an SQL Instance and it listened to network traffic + $dataSet.SqlConnected = $true + $dataSet.Confidence = 'High' + } + #endregion Processing error (Access denied, server error, ...) + + #region Other connection errors + else { + $dataSet.SqlConnected = $false + } + #endregion Other connection errors + } + } + + foreach ($item in $toDelete) { + $masterList.Remove($item) + } + } + + $masterList + } + } + } + + function Get-DomainSPN { + <# + .SYNOPSIS + Returns all computernames with registered MSSQL SPNs. + + .DESCRIPTION + Returns all computernames with registered MSSQL SPNs. + + .PARAMETER DomainController + The domain controller to ask. + + .PARAMETER Credential + The credentials to use while asking. + + .PARAMETER ComputerName + Filter by computername + + .PARAMETER GetSPN + Returns the service SPNs instead of the hostname + + .EXAMPLE + PS C:\> Get-DomainSPN -DomainController $DomainController -Credential $Credential + + Returns all computernames with MSQL SPNs known to $DomainController, assuming credentials are valid. + #> + [CmdletBinding()] + param ( + [string]$DomainController, + [object]$Credential, + [string]$ComputerName = "*", + [switch]$GetSPN + ) + + try { + if ($DomainController) { + if ($Credential) { + $entry = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$DomainController", $Credential.UserName, $Credential.GetNetworkCredential().Password + } + else { + $entry = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$DomainController" + } + } + else { + $entry = [ADSI]'' + } + $objSearcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList $entry + + $objSearcher.PageSize = 200 + $objSearcher.Filter = "(&(objectcategory=computer)(servicePrincipalName=MSSQLsvc*)(|(name=$ComputerName)(dnshostname=$ComputerName)))" + $objSearcher.SearchScope = 'Subtree' + + $results = $objSearcher.FindAll() + foreach ($computer in $results) { + if ($GetSPN) { + $computer.Properties["serviceprincipalname"] | Where-Object { $_ -like "MSSQLsvc*:*" } + } + else { + if ($computer.Properties["dnshostname"]) { + $computer.Properties["dnshostname"][0] + } + else { + $computer.Properties["name"][0] + } + } + } + } + catch { + throw + } + } + + function Get-SQLInstanceBrowserUDP { + <# + .SYNOPSIS + Requests a list of instances from the browser service. + + .DESCRIPTION + Requests a list of instances from the browser service. + + .PARAMETER ComputerName + Computer name or IP address to enumerate SQL Instance from. + + .PARAMETER UDPTimeOut + Timeout in seconds. Longer timeout = more accurate. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + PS C:\> Get-SQLInstanceBrowserUDP -ComputerName 'sql2017' + + Contacts the browsing service on sql2017 and requests its instance information. + + .NOTES + Original Author: Eric Gruber + Editors: + - Scott Sutherland (Pipeline and timeout mods) + - Friedrich Weinmann (Cleanup & dbatools Standardization) + + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)][DbaInstance[]]$ComputerName, + [int]$UDPTimeOut = 2, + [switch]$EnableException + ) + + process { + foreach ($computer in $ComputerName) { + try { + #region Connect to browser service and receive response + $UDPClient = New-Object -TypeName System.Net.Sockets.Udpclient + $UDPClient.Client.ReceiveTimeout = $UDPTimeOut * 1000 + $UDPClient.Connect($computer.ComputerName, 1434) + $UDPPacket = 0x03 + $UDPEndpoint = New-Object -TypeName System.Net.IpEndPoint -ArgumentList ([System.Net.Ipaddress]::Any, 0) + $UDPClient.Client.Blocking = $true + [void]$UDPClient.Send($UDPPacket, $UDPPacket.Length) + $BytesRecived = $UDPClient.Receive([ref]$UDPEndpoint) + # Skip first three characters, since those contain trash data (SSRP metadata) + #$Response = [System.Text.Encoding]::ASCII.GetString($BytesRecived[3..($BytesRecived.Length - 1)]) + $Response = [System.Text.Encoding]::ASCII.GetString($BytesRecived) + #endregion Connect to browser service and receive response + + #region Parse Output + $Response | Select-String "(ServerName;(\w+);InstanceName;(\w+);IsClustered;(\w+);Version;(\d+\.\d+\.\d+\.\d+);(tcp;(\d+)){0,1})" -AllMatches | Select-Object -ExpandProperty Matches | ForEach-Object { + $obj = New-Object Sqlcollaborative.Dbatools.Discovery.DbaBrowserReply -Property @{ + MachineName = $computer.ComputerName + ComputerName = $_.Groups[2].Value + SqlInstance = "$($_.Groups[2].Value)\$($_.Groups[3].Value)" + InstanceName = $_.Groups[3].Value + Version = $_.Groups[5].Value + IsClustered = "Yes" -eq $_.Groups[4].Value + } + if ($_.Groups[7].Success) { + $obj.TCPPort = $_.Groups[7].Value + } + $obj + } + #endregion Parse Output + + $UDPClient.Close() + } + catch { + try { + $UDPClient.Close() + } + catch { + } + + if ($EnableException) { throw } + } + } + } + } + + function Test-TcpPort { + <# + .SYNOPSIS + Tests whether a TCP Port is open or not. + + .DESCRIPTION + Tests whether a TCP Port is open or not. + + .PARAMETER ComputerName + The name of the computer to scan. + + .PARAMETER Port + The port(s) to scan. + + .EXAMPLE + PS C:\> $ports | Test-TcpPort -ComputerName "foo" + + Tests for each port in $ports whether the TCP port is open on computer "foo" + #> + [CmdletBinding()] + param ( + [DbaInstance]$ComputerName, + [Parameter(ValueFromPipeline = $true)][int[]]$Port + ) + + begin { + $client = New-Object Net.Sockets.TcpClient + } + process { + foreach ($item in $Port) { + try { + $client.Connect($ComputerName.ComputerName, $item) + if ($client.Connected) { + $client.Close() + New-Object -TypeName Sqlcollaborative.Dbatools.Discovery.DbaPortReport -ArgumentList $ComputerName.ComputerName, $item, $true + } + else { + New-Object -TypeName Sqlcollaborative.Dbatools.Discovery.DbaPortReport -ArgumentList $ComputerName.ComputerName, $item, $false + } + } + catch { + New-Object -TypeName Sqlcollaborative.Dbatools.Discovery.DbaPortReport -ArgumentList $ComputerName.ComputerName, $item, $false + } + } + } + } + + function Get-IPrange { + <# + .SYNOPSIS + Get the IP addresses in a range + + .DESCRIPTION + A detailed description of the Get-IPrange function. + + .PARAMETER Start + A description of the Start parameter. + + .PARAMETER End + A description of the End parameter. + + .PARAMETER IPAddress + A description of the IPAddress parameter. + + .PARAMETER Mask + A description of the Mask parameter. + + .PARAMETER Cidr + A description of the Cidr parameter. + + .EXAMPLE + Get-IPrange -Start 192.168.8.2 -End 192.168.8.20 + + .EXAMPLE + Get-IPrange -IPAddress 192.168.8.2 -Mask 255.255.255.0 + + .EXAMPLE + Get-IPrange -IPAddress 192.168.8.3 -Cidr 24 + + .NOTES + Author: BarryCWT + Reference: https://gallery.technet.microsoft.com/scriptcenter/List-the-IP-addresses-in-a-60c5bb6b + #> + + param + ( + [string]$Start, + [string]$End, + [string]$IPAddress, + [string]$Mask, + [int]$Cidr + ) + + function IP-toINT64 { + param ($ip) + + $octets = $ip.split(".") + return [int64]([int64]$octets[0] * 16777216 + [int64]$octets[1] * 65536 + [int64]$octets[2] * 256 + [int64]$octets[3]) + } + + function INT64-toIP { + param ([int64]$int) + + return ([System.Net.IPAddress](([math]::truncate($int/16777216)).tostring() + "." + ([math]::truncate(($int % 16777216)/65536)).tostring() + "." + ([math]::truncate(($int % 65536)/256)).tostring() + "." + ([math]::truncate($int % 256)).tostring())) + } + + if ($Cidr) { + $maskaddr = [Net.IPAddress]::Parse((INT64-toIP -int ([convert]::ToInt64(("1" * $Cidr + "0" * (32 - $Cidr)), 2)))) + } + if ($Mask) { + $maskaddr = [Net.IPAddress]::Parse($Mask) + } + if ($IPAddress) { + $ipaddr = [Net.IPAddress]::Parse($IPAddress) + $networkaddr = new-object net.ipaddress ($maskaddr.address -band $ipaddr.address) + $broadcastaddr = new-object net.ipaddress (([system.net.ipaddress]::parse("255.255.255.255").address -bxor $maskaddr.address -bor $networkaddr.address)) + $startaddr = IP-toINT64 -ip $networkaddr.ipaddresstostring + $endaddr = IP-toINT64 -ip $broadcastaddr.ipaddresstostring + } + else { + $startaddr = IP-toINT64 -ip $Start + $endaddr = IP-toINT64 -ip $End + } + + for ($i = $startaddr; $i -le $endaddr; $i++) { + INT64-toIP -int $i + } + } + + function Resolve-IPRange { + <# + .SYNOPSIS + Returns a number of IPAddresses based on range specified. + + .DESCRIPTION + Returns a number of IPAddresses based on range specified. + Warning: A too large range can lead to memory exceptions. + + Scans subnet of active computer if no address is specified. + + .PARAMETER IpAddress + The address / range / mask / cidr to scan. Example input: + - 10.1.1.1 + - 10.1.1.1/24 + - 10.1.1.1-10.1.1.254 + - 10.1.1.1/255.255.255.0 + #> + [CmdletBinding()] + param ( + [AllowEmptyString()][string]$IpAddress + ) + + #region Scan defined range + if ($IpAddress) { + #region Determine processing mode + $mode = 'Unknown' + if ($IpAddress -like "*/*") { + $parts = $IpAddress.Split("/") + + $address = $parts[0] + if ($parts[1] -match ([dbargx]::IPv4)) { + $mask = $parts[1] + $mode = 'Mask' + } + elseif ($parts[1] -as [int]) { + $cidr = [int]$parts[1] + + if (($cidr -lt 8) -or ($cidr -gt 31)) { + throw "$IpAddress does not contain a valid cidr mask!" + } + + $mode = 'CIDR' + } + else { + throw "$IpAddress is not a valid IP Range!" + } + } + elseif ($IpAddress -like "*-*") { + $rangeStart = $IpAddress.Split("-")[0] + $rangeEnd = $IpAddress.Split("-")[1] + + if ($rangeStart -notmatch ([dbargx]::IPv4)) { + throw "$IpAddress is not a valid IP Range!" + } + if ($rangeEnd -notmatch ([dbargx]::IPv4)) { + throw "$IpAddress is not a valid IP Range!" + } + + $mode = 'Range' + } + else { + if ($IpAddress -notmatch ([dbargx]::IPv4)) { + throw "$IpAddress is not a valid IP Address!" + } + return $IpAddress + } + #endregion Determine processing mode + + switch ($mode) { + 'CIDR' { + Get-IPrange -IPAddress $address -Cidr $cidr + } + 'Mask' { + Get-IPrange -IPAddress $address -Mask $mask + } + 'Range' { + Get-IPrange -Start $rangeStart -End $rangeEnd + } + } + } + #endregion Scan defined range + + #region Scan own computer range + else { + foreach ($interface in ([System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | Where-Object NetworkInterfaceType -Like '*Ethernet*')) { + foreach ($property in ($interface.GetIPProperties().UnicastAddresses | Where-Object { $_.Address.AddressFamily -like "InterNetwork" })) { + Get-IPrange -IPAddress $property.Address -Cidr $property.PrefixLength + } + } + } + #endregion Scan own computer range + } + #endregion Utility Functions + + #region Build parameter Splat for scan + $paramTestSqlInstance = @{ + ScanType = $ScanType + TCPPort = $TCPPort + EnableException = $EnableException + MinimumConfidence = $MinimumConfidence + } + + # Only specify when passed by user to avoid credential prompts on PS3/4 + if ($SqlCredential) { + $paramTestSqlInstance["SqlCredential"] = $SqlCredential + } + if ($Credential) { + $paramTestSqlInstance["Credential"] = $Credential + } + if ($DomainController) { + $paramTestSqlInstance["DomainController"] = $DomainController + } + #endregion Build parameter Splat for scan + + # Prepare item processing in a pipeline compliant way + $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Test-SqlInstance', [System.Management.Automation.CommandTypes]::Function) + $scriptCmd = { + & $wrappedCmd @paramTestSqlInstance + } + $steppablePipeline = $scriptCmd.GetSteppablePipeline() + $steppablePipeline.Begin($true) + } + + process { + if (Test-FunctionInterrupt) { return } + #region Process items or discover stuff + switch ($PSCmdlet.ParameterSetName) { + 'Computer' { + $ComputerName | Invoke-SteppablePipeline -Pipeline $steppablePipeline + } + 'Discover' { + #region Discovery: DataSource Enumeration + if ($DiscoveryType -band ([Sqlcollaborative.Dbatools.Discovery.DbaInstanceDiscoveryType]::DataSourceEnumeration)) { + try { + # Discover instances + foreach ($instance in ([System.Data.Sql.SqlDataSourceEnumerator]::Instance.GetDataSources())) { + if ($instance.InstanceName -ne [System.DBNull]::Value) { + $steppablePipeline.Process("$($instance.Servername)\$($instance.InstanceName)") + } + else { + $steppablePipeline.Process($instance.Servername) + } + } + } + catch { + Write-Message -Level Warning -Message "Datasource enumeration failed" -ErrorRecord $_ -EnableException $EnableException + } + } + #endregion Discovery: DataSource Enumeration + + #region Discovery: SPN Search + if ($DiscoveryType -band ([Sqlcollaborative.Dbatools.Discovery.DbaInstanceDiscoveryType]::Domain)) { + try { + Get-DomainSPN -DomainController $DomainController -Credential $Credential -ErrorAction Stop | Invoke-SteppablePipeline -Pipeline $steppablePipeline + } + catch { + Write-Message -Level Warning -Message "Failed to execute Service Principal Name discovery" -ErrorRecord $_ -EnableException $EnableException + } + } + #endregion Discovery: SPN Search + + #region Discovery: IP Range + if ($DiscoveryType -band ([Sqlcollaborative.Dbatools.Discovery.DbaInstanceDiscoveryType]::IPRange)) { + if ($IpAddress) { + foreach ($address in $IpAddress) { + Resolve-IPRange -IpAddress $address | Invoke-SteppablePipeline -Pipeline $steppablePipeline + } + } + else { + Resolve-IPRange | Invoke-SteppablePipeline -Pipeline $steppablePipeline + } + } + #endregion Discovery: IP Range + } + default { + Stop-Function -Message "Invalid parameterset, some developer probably had a beer too much. Please file an issue so we can fix this" -EnableException $EnableException + return + } + } + #endregion Process items or discover stuff + } + + end { + if (Test-FunctionInterrupt) { + return + } + $steppablePipeline.End() + } +} +#ValidationTags#Messaging# +function Find-DbaLoginInGroup { + <# + .SYNOPSIS + Finds Logins in Active Directory groups that have logins on the SQL Instance. + + .DESCRIPTION + Outputs all the active directory groups members for a server, or limits it to find a specific AD user in the groups + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a + collection and receive pipeline input. + + .PARAMETER SqlCredential + PSCredential object to connect under. If not specified, current Windows login will be used. + + .PARAMETER Login + Find all AD Groups used on the instance that an individual login is a member of. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Login, AD, ActiveDirectory, Group, Security + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Author: Simone Bizzotto, @niphlod + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaLoginInGroup + + .EXAMPLE + Find-DbaLoginInGroup -SqlInstance DEV01 -Login "MyDomain\Stephen.Bennett" + + Returns all active directory groups with logins on Sql Instance DEV01 that contain the AD user Stephen.Bennett. + + .EXAMPLE + Find-DbaLoginInGroup -SqlInstance DEV01 + + Returns all active directory users within all windows AD groups that have logins on the instance. + + .EXAMPLE + Find-DbaLoginInGroup -SqlInstance DEV01 | Where-Object Login -like '*stephen*' + + Returns all active directory users within all windows AD groups that have logins on the instance whose login contains 'stephen' + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Login, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + try { + Add-Type -AssemblyName System.DirectoryServices.AccountManagement + } + catch { + Stop-Function -Message "Failed to load Assembly needed" -ErrorRecord $_ + } + + function Get-AllLogins { + param + ( + [string]$ADGroup, + [string[]]$discard, + [string]$ParentADGroup + ) + begin { + $output = @() + } + process { + try { + $domain = $AdGroup.Split("\")[0] + $ads = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain', $domain) + [string]$groupName = $AdGroup + $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($ads, $groupName); + $subgroups = @() + foreach ($member in $group.Members) { + $memberDomain = $member.DistinguishedName -Split "," | Where-Object { $_ -like "DC=*" } | Select-Object -first 1 | ForEach-Object { $_.ToUpper() -replace "DC=", '' } + if ($member.StructuralObjectClass -eq "group") { + $fullName = $memberDomain + "\" + $member.SamAccountName + if ($fullName -in $discard) { + Write-Message -Level Verbose -Message "skipping $fullName, already enumerated" + continue + } + else { + $subgroups += $fullName + } + } + else { + $output += [PSCustomObject]@{ + SqlInstance = $server.Name + InstanceName = $server.ServiceName + ComputerName = $server.NetName + Login = $memberDomain + "\" + $member.SamAccountName + DisplayName = $member.DisplayName + MemberOf = $AdGroup + ParentADGroupLogin = $ParentADGroup + } + } + } + } + catch { + Stop-Function -Message "Failed to connect to Group: $member." -Target $member -ErrorRecord $_ + } + $discard += $ADGroup + foreach ($gr in $subgroups) { + if ($gr -notin $discard) { + $discard += $gr + Write-Message -Level Verbose -Message "Looking at $gr, recursively." + Get-AllLogins -ADGroup $gr -discard $discard -ParentADGroup $ParentADGroup + } + } + } + end { + $output + } + } + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $AdGroups = $server.Logins | Where-Object { $_.LoginType -eq "WindowsGroup" -and $_.Name -ne "BUILTIN\Administrators" -and $_.Name -notlike "*NT SERVICE*" } + + foreach ($AdGroup in $AdGroups) { + Write-Message -Level Verbose -Message "Looking at Group: $AdGroup" + $ADGroupOut += Get-AllLogins $AdGroup.Name -ParentADGroup $AdGroup.Name + } + + if (-not $Login) { + $res = $ADGroupOut + } + else { + $res = $ADGroupOut | Where-Object { $Login -contains $_.Login } + if ($res.Length -eq 0) { + Write-Message -Level Warning -Message "No logins matching $($Login -join ',') found connecting to $server" + continue + } + } + Select-DefaultView -InputObject $res -Property SqlInstance, Login, DisplayName, MemberOf, ParentADGroupLogin + } + } +} +#ValidationTags#FlowControl,Pipeline# +function Find-DbaOrphanedFile { + <# + .SYNOPSIS + Find-DbaOrphanedFile finds orphaned database files. Orphaned database files are files not associated with any attached database. + + .DESCRIPTION + This command searches all directories associated with SQL database files for database files that are not currently in use by the SQL Server instance. + + By default, it looks for orphaned .mdf, .ldf and .ndf files in the root\data directory, the default data path, the default log path, the system paths and any directory in use by any attached directory. + + You can specify additional filetypes using the -FileType parameter, and additional paths to search using the -Path parameter. + + .PARAMETER SqlInstance + The SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies one or more directories to search in addition to the default data and log directories. + + .PARAMETER FileType + Specifies file extensions other than mdf, ldf and ndf to search for. Do not include the dot (".") when specifying the extension. + + .PARAMETER LocalOnly + If this switch is enabled, only local filenames will be returned. Using this switch with multiple servers is not recommended since it does not return the associated server name. + + .PARAMETER RemoteOnly + If this switch is enabled, only remote filenames will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Orphan, Database, DatabaseFile + Author: Sander Stad (@sqlstad), sqlstad.nl + Requires: sysadmin access on SQL Servers + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + Thanks to Paul Randal's notes on FILESTREAM which can be found at http://www.sqlskills.com/blogs/paul/filestream-directory-structure/ + + .LINK + https://dbatools.io/Find-DbaOrphanedFile + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sqlserver2014a + + Connects to sqlserver2014a, authenticating with Windows credentials, and searches for orphaned files. Returns server name, local filename, and unc path to file. + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sqlserver2014a -SqlCredential $cred + + Connects to sqlserver2014a, authenticating with SQL Server authentication, and searches for orphaned files. Returns server name, local filename, and unc path to file. + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sql2014 -Path 'E:\Dir1', 'E:\Dir2' + + Finds the orphaned files in "E:\Dir1" and "E:Dir2" in addition to the default directories. + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sql2014 -LocalOnly + + Returns only the local filepaths for orphaned files. + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sql2014 -RemoteOnly + + Returns only the remote filepath for orphaned files. + + .EXAMPLE + Find-DbaOrphanedFile -SqlInstance sql2014, sql2016 -FileType fsf, mld + + Finds the orphaned ending with ".fsf" and ".mld" in addition to the default filetypes ".mdf", ".ldf", ".ndf" for both the servers sql2014 and sql2016. + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(Mandatory = $false)] + [object]$SqlCredential, + [parameter(Mandatory = $false)] + [string[]]$Path, + [string[]]$FileType, + [switch]$LocalOnly, + [switch]$RemoteOnly, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Get-SQLDirTreeQuery { + param($PathList) + # use sysaltfiles in lower versions + + $q1 = "CREATE TABLE #enum ( id int IDENTITY, fs_filename nvarchar(512), depth int, is_file int, parent nvarchar(512) ); DECLARE @dir nvarchar(512);" + $q2 = "SET @dir = 'dirname'; + + INSERT INTO #enum( fs_filename, depth, is_file ) + EXEC xp_dirtree @dir, 1, 1; + + UPDATE #enum + SET parent = @dir, + fs_filename = ltrim(rtrim(fs_filename)) + WHERE parent IS NULL;" + + $query_files_sql = "SELECT e.fs_filename AS filename, e.parent + FROM #enum AS e + WHERE e.fs_filename NOT IN( 'xtp', '5', '`$FSLOG', '`$HKv2', 'filestream.hdr' ) + AND is_file = 1;" + + # build the query string based on how many directories they want to enumerate + $sql = $q1 + $sql += $($PathList | Where-Object { $_ -ne '' } | ForEach-Object { "$([System.Environment]::Newline)$($q2 -Replace 'dirname', $_)" }) + $sql += $query_files_sql + Write-Message -Level Debug -Message $sql + return $sql + } + function Get-SqlFileStructure { + param + ( + [Parameter(Mandatory = $true, Position = 1)] + [Microsoft.SqlServer.Management.Smo.SqlSmoObject]$smoserver + ) + if ($smoserver.versionMajor -eq 8) { + $sql = "select filename from sysaltfiles" + } + else { + $sql = "select physical_name as filename from sys.master_files" + } + + $dbfiletable = $smoserver.ConnectionContext.ExecuteWithResults($sql) + $ftfiletable = $dbfiletable.Tables[0].Clone() + $dbfiletable.Tables[0].TableName = "data" + + # Add support for Full Text Catalogs in Sql Server 2005 and below + if ($server.VersionMajor -lt 10) { + $databaselist = $smoserver.Databases | Select-Object -property Name, IsFullTextEnabled + foreach ($db in $databaselist) { + if ($db.IsFullTextEnabled -eq $false) { + continue + } + $database = $db.name + $fttable = $null = $smoserver.Databases[$database].ExecuteWithResults('sp_help_fulltext_catalogs') + foreach ($ftc in $fttable.Tables[0].rows) { + $null = $ftfiletable.Rows.add($ftc.Path) + } + } + } + + $null = $dbfiletable.Tables.Add($ftfiletable) + return $dbfiletable.Tables.Filename + } + + function Format-Path { + param ($path) + + $path = $path.Trim() + #Thank you windows 2000 + $path = $path -replace '[^A-Za-z0-9 _\.\-\\:]', '__' + return $path + } + + $FileType += "mdf", "ldf", "ndf" + $systemfiles = "distmdl.ldf", "distmdl.mdf", "mssqlsystemresource.ldf", "mssqlsystemresource.mdf" + + $FileTypeComparison = $FileType | ForEach-Object {$_.ToLower()} | Where-Object { $_ } | Sort-Object | Get-Unique + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + # Reset all the arrays + $dirtreefiles = $valid = $paths = $matching = @() + + $filestructure = Get-SqlFileStructure $server + + # Get any paths associated with current data and log files + foreach ($file in $filestructure) { + $paths += Split-Path -Path $file -Parent + } + + # Get the default data and log directories from the instance + Write-Message -Level Debug -Message "Adding paths" + $paths += $server.RootDirectory + "\DATA" + $paths += Get-SqlDefaultPaths $server data + $paths += Get-SqlDefaultPaths $server log + $paths += $server.MasterDBPath + $paths += $server.MasterDBLogPath + $paths += $Path + $paths = $paths | ForEach-Object { "$_".TrimEnd("\") } | Sort-Object | Get-Unique + $sql = Get-SQLDirTreeQuery $paths + $datatable = $server.Databases['master'].ExecuteWithResults($sql).Tables[0] + + foreach ($row in $datatable) { + $fullpath = [IO.Path]::combine($row.parent, $row.filename) + $dirtreefiles += [pscustomobject]@{ + FullPath = $fullpath + Comparison = [IO.Path]::GetFullPath($(Format-Path $fullpath)) + } + } + $dirtreefiles = $dirtreefiles | Where-Object { $_ } | Sort-Object Comparison -Unique + + foreach ($file in $filestructure) { + $valid += [IO.Path]::GetFullPath($(Format-Path $file)) + } + + $valid = $valid | Sort-Object | Get-Unique + + foreach ($file in $dirtreefiles.Comparison) { + foreach ($type in $FileTypeComparison) { + if ($file.ToLower().EndsWith($type)) { + $matching += $file + break + } + } + } + + $dirtreematcher = @{} + foreach ($el in $dirtreefiles) { + $dirtreematcher[$el.Comparison] = $el.Fullpath + } + + foreach ($file in $matching) { + if ($file -notin $valid) { + $fullpath = $dirtreematcher[$file] + + $filename = Split-Path $fullpath -Leaf + + if ($filename -in $systemfiles) { continue } + + $result = [pscustomobject]@{ + Server = $server.name + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Filename = $fullpath + RemoteFilename = Join-AdminUnc -Servername $server.netname -Filepath $fullpath + } + + if ($LocalOnly -eq $true) { + ($result | Select-Object filename).filename + continue + } + + if ($RemoteOnly -eq $true) { + ($result | Select-Object remotefilename).remotefilename + continue + } + + $result | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Filename, RemoteFilename + + } + } + + } + } + end { + if ($result.count -eq 0) { + Write-Message -Level Verbose -Message "No orphaned files found" + } + } +} +function Find-DbaSimilarTable { + <# + .SYNOPSIS + Returns all tables/views that are similar in structure by comparing the column names of matching and matched tables/views + + .DESCRIPTION + This function can either run against specific databases or all databases searching all/specific tables and views including in system databases. + Typically one would use this to find for example archive version(s) of a table whose structures are similar. + This can also be used to find tables/views that are very similar to a given table/view structure to see where a table/view might be used. + + More information can be found here: https://sqljana.wordpress.com/2017/03/31/sql-server-find-tables-with-similar-table-structure/ + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER SchemaName + If you are looking in a specific schema whose table structures is to be used as reference structure, provide the name of the schema. + If no schema is provided, looks at all schemas + + .PARAMETER TableName + If you are looking in a specific table whose structure is to be used as reference structure, provide the name of the table. + If no table is provided, looks at all tables + If the table name exists in multiple schemas, all of them would qualify + + .PARAMETER ExcludeViews + By default, views are included. You can exclude them by setting this switch to $false + This excludes views in both matching and matched list + + .PARAMETER IncludeSystemDatabases + By default system databases are ignored but you can include them within the search using this parameter + + .PARAMETER MatchPercentThreshold + The minimum percentage of column names that should match between the matching and matched objects. + Entries with no matches are eliminated + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Table + Author: Jana Sattainathan (@SQLJana - http://sqljana.wordpress.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaSimilarTable + + .EXAMPLE + Find-DbaSimilarTable -SqlInstance DEV01 + + Searches all user database tables and views for each, returns all tables or views with their matching tables/views and match percent + + .EXAMPLE + Find-DbaSimilarTable -SqlInstance DEV01 -Database AdventureWorks + + Searches AdventureWorks database and lists tables/views and their corresponding matching tables/views with match percent + + .EXAMPLE + Find-DbaSimilarTable -SqlInstance DEV01 -Database AdventureWorks -SchemaName HumanResource + + Searches AdventureWorks database and lists tables/views in the HumanResource schema with their corresponding matching tables/views with match percent + + .EXAMPLE + Find-DbaSimilarTable -SqlInstance DEV01 -Database AdventureWorks -SchemaName HumanResource -Table Employee + + Searches AdventureWorks database and lists tables/views in the HumanResource schema and table Employee with its corresponding matching tables/views with match percent + + .EXAMPLE + Find-DbaSimilarTable -SqlInstance DEV01 -Database AdventureWorks -MatchPercentThreshold 60 + + Searches AdventureWorks database and lists all tables/views with its corresponding matching tables/views with match percent greater than or equal to 60 + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [string]$SchemaName, + [string]$TableName, + [switch]$ExcludeViews, + [switch]$IncludeSystemDatabases, + [int]$MatchPercentThreshold, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $everyServerVwCount = 0 + + $sqlSelect = "WITH ColCountsByTable + AS + ( + SELECT + c.TABLE_CATALOG, + c.TABLE_SCHEMA, + c.TABLE_NAME, + COUNT(1) AS Column_Count + FROM INFORMATION_SCHEMA.COLUMNS c + GROUP BY + c.TABLE_CATALOG, + c.TABLE_SCHEMA, + c.TABLE_NAME + ) + SELECT + 100 * COUNT(c2.COLUMN_NAME) /*Matching_Column_Count*/ / MIN(ColCountsByTable.Column_Count) /*Column_Count*/ AS MatchPercent, + DENSE_RANK() OVER(ORDER BY c.TABLE_CATALOG, c.TABLE_SCHEMA, c.TABLE_NAME) TableNameRankInDB, + c.TABLE_CATALOG AS DatabaseName, + c.TABLE_SCHEMA AS SchemaName, + c.TABLE_NAME AS TableName, + t.TABLE_TYPE AS TableType, + MIN(ColCountsByTable.Column_Count) AS ColumnCount, + c2.TABLE_CATALOG AS MatchingDatabaseName, + c2.TABLE_SCHEMA AS MatchingSchemaName, + c2.TABLE_NAME AS MatchingTableName, + t2.TABLE_TYPE AS MatchingTableType, + COUNT(c2.COLUMN_NAME) AS MatchingColumnCount + FROM INFORMATION_SCHEMA.TABLES t + INNER JOIN INFORMATION_SCHEMA.COLUMNS c + ON t.TABLE_CATALOG = c.TABLE_CATALOG + AND t.TABLE_SCHEMA = c.TABLE_SCHEMA + AND t.TABLE_NAME = c.TABLE_NAME + INNER JOIN ColCountsByTable + ON t.TABLE_CATALOG = ColCountsByTable.TABLE_CATALOG + AND t.TABLE_SCHEMA = ColCountsByTable.TABLE_SCHEMA + AND t.TABLE_NAME = ColCountsByTable.TABLE_NAME + LEFT OUTER JOIN INFORMATION_SCHEMA.COLUMNS c2 + ON t.TABLE_NAME != c2.TABLE_NAME + AND c.COLUMN_NAME = c2.COLUMN_NAME + LEFT JOIN INFORMATION_SCHEMA.TABLES t2 + ON c2.TABLE_NAME = t2.TABLE_NAME" + + $sqlWhere = " + WHERE " + + $sqlGroupBy = " + GROUP BY + c.TABLE_CATALOG, + c.TABLE_SCHEMA, + c.TABLE_NAME, + t.TABLE_TYPE, + c2.TABLE_CATALOG, + c2.TABLE_SCHEMA, + c2.TABLE_NAME, + t2.TABLE_TYPE " + + $sqlHaving = " + HAVING + /*Match_Percent should be greater than 0 at minimum!*/ + " + + $sqlOrderBy = " + ORDER BY + MatchPercent DESC" + + + $sql = '' + $wherearray = @() + + if ($ExcludeViews) { + $wherearray += " (t.TABLE_TYPE <> 'VIEW' AND t2.TABLE_TYPE <> 'VIEW') " + } + + if ($SchemaName) { + $wherearray += (" (c.TABLE_SCHEMA = '{0}') " -f $SchemaName.Replace("'", "''")) #Replace single quotes with two single quotes! + } + + if ($TableName) { + $wherearray += (" (c.TABLE_NAME = '{0}') " -f $TableName.Replace("'", "''")) #Replace single quotes with two single quotes! + + } + + if ($wherearray.length -gt 0) { + $sqlWhere = "$sqlWhere " + ($wherearray -join " AND ") + } + else { + $sqlWhere = "" + } + + + $matchThreshold = 0 + if ($MatchPercentThreshold) { + $matchThreshold = $MatchPercentThreshold + } + else { + $matchThreshold = 0 + } + + $sqlHaving += (" (100 * COUNT(c2.COLUMN_NAME) / MIN(ColCountsByTable.Column_Count) >= {0}) " -f $matchThreshold) + + + + $sql = "$sqlSelect $sqlWhere $sqlGroupBy $sqlHaving $sqlOrderBy" + + Write-Message -Level Debug -Message $sql + + } + + process { + foreach ($Instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + + #Use IsAccessible instead of Status -eq 'normal' because databases that are on readable secondaries for AG or mirroring replicas will cause errors to be thrown + if ($IncludeSystemDatabases) { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -eq $true } + } + else { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -eq $true -and $_.IsSystemObject -eq $false } + } + + if ($Database) { + $dbs = $server.Databases | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + + $totalCount = 0 + $dbCount = $dbs.count + foreach ($db in $dbs) { + + Write-Message -Level Verbose -Message "Searching on database $db" + $rows = $db.Query($sql) + + foreach ($row in $rows) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Table = "$($row.DatabaseName).$($row.SchemaName).$($row.TableName)" + MatchingTable = "$($row.MatchingDatabaseName).$($row.MatchingSchemaName).$($row.MatchingTableName)" + MatchPercent = $row.MatchPercent + OriginalDatabaseName = $row.DatabaseName + OriginalSchemaName = $row.SchemaName + OriginalTableName = $row.TableName + OriginalTableNameRankInDB = $row.TableNameRankInDB + OriginalTableType = $row.TableType + OriginalColumnCount = $row.ColumnCount + MatchingDatabaseName = $row.MatchingDatabaseName + MatchingSchemaName = $row.MatchingSchemaName + MatchingTableName = $row.MatchingTableName + MatchingTableType = $row.MatchingTableType + MatchingColumnCount = $row.MatchingColumnCount + } + } + + $vwCount = $vwCount + $rows.Count + $totalCount = $totalCount + $rows.Count + $everyServerVwCount = $everyServerVwCount + $rows.Count + + Write-Message -Level Verbose -Message "Found $vwCount tables/views in $db" + } + + Write-Message -Level Verbose -Message "Found $totalCount total tables/views in $dbCount databases" + } + } + end { + Write-Message -Level Verbose -Message "Found $everyServerVwCount total tables/views" + } +} +function Find-DbaStoredProcedure { + <# + .SYNOPSIS + Returns all stored procedures that contain a specific case-insensitive string or regex pattern. + + .DESCRIPTION + This function can either run against specific databases or all databases searching all user or user and system stored procedures. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Pattern + String pattern that you want to search for in the stored procedure textbody + + .PARAMETER IncludeSystemObjects + By default, system stored procedures are ignored but you can include them within the search using this parameter. + + Warning - this will likely make it super slow if you run it on all databases. + + .PARAMETER IncludeSystemDatabases + By default system databases are ignored but you can include them within the search using this parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: StoredProcedure, Proc + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaStoredProcedure + + .EXAMPLE + Find-DbaStoredProcedure -SqlInstance DEV01 -Pattern whatever + + Searches all user databases stored procedures for "whatever" in the textbody + + .EXAMPLE + Find-DbaStoredProcedure -SqlInstance sql2016 -Pattern '\w+@\w+\.\w+' + + Searches all databases for all stored procedures that contain a valid email pattern in the textbody + + .EXAMPLE + Find-DbaStoredProcedure -SqlInstance DEV01 -Database MyDB -Pattern 'some string' -Verbose + + Searches in "mydb" database stored procedures for "some string" in the textbody + + .EXAMPLE + Find-DbaStoredProcedure -SqlInstance sql2016 -Database MyDB -Pattern RUNTIME -IncludeSystemObjects + + Searches in "mydb" database stored procedures for "runtime" in the textbody + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $true)] + [string]$Pattern, + [switch]$IncludeSystemObjects, + [switch]$IncludeSystemDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = "SELECT OBJECT_SCHEMA_NAME(p.object_id) as ProcSchema, p.name, m.definition as TextBody FROM sys.sql_modules m, sys.procedures p WHERE m.object_id = p.object_id" + if (!$IncludeSystemObjects) { $sql = "$sql AND p.is_ms_shipped = 0" } + $everyserverspcount = 0 + } + process { + foreach ($Instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $Instance" + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $Instance" + continue + } + + if ($server.versionMajor -lt 9) { + Write-Message -Level Warning -Message "This command only supports SQL Server 2005 and above." + Continue + } + + if ($IncludeSystemDatabases) { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" } + } + else { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" -and $_.IsSystemObject -eq $false } + } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + $totalcount = 0 + $dbcount = $dbs.count + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Searching on database $db" + + # If system objects aren't needed, find stored procedure text using SQL + # This prevents SMO from having to enumerate + + if (!$IncludeSystemObjects) { + Write-Message -Level Debug -Message $sql + $rows = $db.ExecuteWithResults($sql).Tables.Rows + $sproccount = 0 + + foreach ($row in $rows) { + $totalcount++; $sproccount++; $everyserverspcount++ + + $procSchema = $row.ProcSchema + $proc = $row.Name + + Write-Message -Level Verbose -Message "Looking in stored procedure: $procSchema.$proc textBody for $pattern" + if ($row.TextBody -match $Pattern) { + $sp = $db.StoredProcedures | Where-Object {$_.Schema -eq $procSchema -and $_.Name -eq $proc} + + $StoredProcedureText = $sp.TextBody.split("`n") + $spTextFound = $StoredProcedureText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + Database = $db.Name + Schema = $sp.Schema + Name = $sp.Name + Owner = $sp.Owner + IsSystemObject = $sp.IsSystemObject + CreateDate = $sp.CreateDate + LastModified = $sp.DateLastModified + StoredProcedureTextFound = $spTextFound -join "`n" + StoredProcedure = $sp + StoredProcedureFullText = $sp.TextBody + } | Select-DefaultView -ExcludeProperty StoredProcedure, StoredProcedureFullText + } + } + } + else { + $storedprocedures = $db.StoredProcedures + + foreach ($sp in $storedprocedures) { + $totalcount++; $sproccount++; $everyserverspcount++ + + $procSchema = $sp.Schema + $proc = $sp.Name + + Write-Message -Level Verbose -Message "Looking in stored procedure $procSchema.$proc textBody for $pattern" + if ($sp.TextBody -match $Pattern) { + + $StoredProcedureText = $sp.TextBody.split("`n") + $spTextFound = $StoredProcedureText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + Database = $db.Name + Schema = $sp.Schema + Name = $sp.Name + Owner = $sp.Owner + IsSystemObject = $sp.IsSystemObject + CreateDate = $sp.CreateDate + LastModified = $sp.DateLastModified + StoredProcedureTextFound = $spTextFound -join "`n" + StoredProcedure = $sp + StoredProcedureFullText = $sp.TextBody + } | Select-DefaultView -ExcludeProperty StoredProcedure, StoredProcedureFullText + } + } + } + Write-Message -Level Verbose -Message "Evaluated $sproccount stored procedures in $db" + } + Write-Message -Level Verbose -Message "Evaluated $totalcount total stored procedures in $dbcount databases" + } + } + end { + Write-Message -Level Verbose -Message "Evaluated $everyserverspcount total stored procedures" + } +} +function Find-DbaTrigger { + <# + .SYNOPSIS + Returns all triggers that contain a specific case-insensitive string or regex pattern. + + .DESCRIPTION + This function search on Instance, Database and Object level. + If you specify one or more databases, search on Server level will not be preformed. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Pattern + String pattern that you want to search for in the trigger textbody + + .PARAMETER TriggerLevel + Allows specify the trigger level that you want to search. By default is All (Server, Database, Object). + + .PARAMETER IncludeSystemObjects + By default, system triggers are ignored but you can include them within the search using this parameter. + + Warning - this will likely make it super slow if you run it on all databases. + + .PARAMETER IncludeSystemDatabases + By default system databases are ignored but you can include them within the search using this parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Trigger + Author: Cláudio Silva, @ClaudioESSilva + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaTrigger + + .EXAMPLE + Find-DbaTrigger -SqlInstance DEV01 -Pattern whatever + + Searches all user databases triggers for "whatever" in the textbody + + .EXAMPLE + Find-DbaTrigger -SqlInstance sql2016 -Pattern '\w+@\w+\.\w+' + + Searches all databases for all triggers that contain a valid email pattern in the textbody + + .EXAMPLE + Find-DbaTrigger -SqlInstance DEV01 -Database MyDB -Pattern 'some string' -Verbose + + Searches in "mydb" database triggers for "some string" in the textbody + + .EXAMPLE + Find-DbaTrigger -SqlInstance sql2016 -Database MyDB -Pattern RUNTIME -IncludeSystemObjects + + Searches in "mydb" database triggers for "runtime" in the textbody + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $true)] + [string]$Pattern, + [ValidateSet('All', 'Server', 'Database', 'Object')] + [string]$TriggerLevel = 'All', + [switch]$IncludeSystemObjects, + [switch]$IncludeSystemDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sqlDatabaseTriggers = "SELECT tr.name, m.definition as TextBody FROM sys.sql_modules m, sys.triggers tr WHERE m.object_id = tr.object_id AND tr.parent_class = 0" + + $sqlTableTriggers = "SELECT OBJECT_SCHEMA_NAME(tr.parent_id) TableSchema, OBJECT_NAME(tr.parent_id) AS TableName, tr.name, m.definition as TextBody FROM sys.sql_modules m, sys.triggers tr WHERE m.object_id = tr.object_id AND tr.parent_class = 1" + if (!$IncludeSystemObjects) { $sqlTableTriggers = "$sqlTableTriggers AND tr.is_ms_shipped = 0" } + + $everyserverstcount = 0 + } + process { + foreach ($Instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $Instance" + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $Instance" + continue + } + + if ($server.versionMajor -lt 9) { + Write-Message -Level Warning -Message "This command only supports SQL Server 2005 and above." + Continue + } + + #search at instance level. Only if no database was specified + if ((-Not $Database) -and ($TriggerLevel -in @('All', 'Server'))) { + foreach ($trigger in $server.Triggers) { + $everyserverstcount++; $triggercount++ + Write-Message -Level Debug -Message "Looking in Trigger: $trigger TextBody for $pattern" + if ($trigger.TextBody -match $Pattern) { + + $triggerText = $trigger.TextBody.split("`n`r") + $trTextFound = $triggerText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + TriggerLevel = "Server" + Database = $null + Object = $null + Name = $trigger.Name + IsSystemObject = $trigger.IsSystemObject + CreateDate = $trigger.CreateDate + LastModified = $trigger.DateLastModified + TriggerTextFound = $trTextFound -join "`n" + Trigger = $trigger + TriggerFullText = $trigger.TextBody + } | Select-DefaultView -ExcludeProperty Trigger, TriggerFullText + } + } + Write-Message -Level Verbose -Message "Evaluated $triggercount triggers in $server" + } + + if ($IncludeSystemDatabases) { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" } + } + else { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" -and $_.IsSystemObject -eq $false } + } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + $totalcount = 0 + $dbcount = $dbs.count + + if ($TriggerLevel -in @('All', 'Database', 'Object')) { + foreach ($db in $dbs) { + + Write-Message -Level Verbose -Message "Searching on database $db" + + # If system objects aren't needed, find trigger text using SQL + # This prevents SMO from having to enumerate + + if (!$IncludeSystemObjects) { + if ($TriggerLevel -in @('All', 'Database')) { + #Get Database Level triggers (DDL) + Write-Message -Level Debug -Message $sqlDatabaseTriggers + $rows = $db.ExecuteWithResults($sqlDatabaseTriggers).Tables.Rows + $triggercount = 0 + + foreach ($row in $rows) { + $totalcount++; $triggercount++; $everyserverstcount++ + + $trigger = $row.name + + Write-Message -Level Verbose -Message "Looking in trigger $trigger for textBody with pattern $pattern on database $db" + if ($row.TextBody -match $Pattern) { + $tr = $db.Triggers | Where-Object name -eq $row.name + + $triggerText = $tr.TextBody.split("`n`r") + $trTextFound = $triggerText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + TriggerLevel = "Database" + Database = $db.name + Object = $tr.Parent + Name = $tr.Name + IsSystemObject = $tr.IsSystemObject + CreateDate = $tr.CreateDate + LastModified = $tr.DateLastModified + TriggerTextFound = $trTextFound -join "`n" + Trigger = $tr + TriggerFullText = $tr.TextBody + } | Select-DefaultView -ExcludeProperty Trigger, TriggerFullText + } + } + } + + if ($TriggerLevel -in @('All', 'Object')) { + #Get Object Level triggers (DML) + Write-Message -Level Debug -Message $sqlTableTriggers + $rows = $db.ExecuteWithResults($sqlTableTriggers).Tables.Rows + $triggercount = 0 + + foreach ($row in $rows) { + $totalcount++; $triggercount++; $everyserverstcount++ + + $trigger = $row.name + $triggerParentSchema = $row.TableSchema + $triggerParent = $row.TableName + + Write-Message -Level Verbose -Message "Looking in trigger $trigger for textBody with pattern $pattern in object $triggerParentSchema.$triggerParent at database $db" + if ($row.TextBody -match $Pattern) { + + $tr = ($db.Tables | Where-Object {$_.Name -eq $triggerParent -and $_.Schema -eq $triggerParentSchema}).Triggers | Where-Object name -eq $row.name + + $triggerText = $tr.TextBody.split("`n`r") + $trTextFound = $triggerText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + TriggerLevel = "Object" + Database = $db.name + Object = $tr.Parent + Name = $tr.Name + IsSystemObject = $tr.IsSystemObject + CreateDate = $tr.CreateDate + LastModified = $tr.DateLastModified + TriggerTextFound = $trTextFound -join "`n" + Trigger = $tr + TriggerFullText = $tr.TextBody + } | Select-DefaultView -ExcludeProperty Trigger, TriggerFullText + } + } + } + } + else { + if ($TriggerLevel -in @('All', 'Database')) { + #Get Database Level triggers (DDL) + $triggers = $db.Triggers + + $triggercount = 0 + + foreach ($tr in $triggers) { + $totalcount++; $triggercount++; $everyserverstcount++ + $trigger = $tr.Name + + Write-Message -Level Verbose -Message "Looking in trigger $trigger for textBody with pattern $pattern on database $db" + if ($tr.TextBody -match $Pattern) { + + $triggerText = $tr.TextBody.split("`n`r") + $trTextFound = $triggerText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + TriggerLevel = "Database" + Database = $db.name + Object = $tr.Parent + Name = $tr.Name + IsSystemObject = $tr.IsSystemObject + CreateDate = $tr.CreateDate + LastModified = $tr.DateLastModified + TriggerTextFound = $trTextFound -join "`n" + Trigger = $tr + TriggerFullText = $tr.TextBody + } | Select-DefaultView -ExcludeProperty Trigger, TriggerFullText + } + } + } + + if ($TriggerLevel -in @('All', 'Object')) { + #Get Object Level triggers (DML) + $triggers = $db.Tables | ForEach-Object {$_.Triggers} + + $triggercount = 0 + + foreach ($tr in $triggers) { + $totalcount++; $triggercount++; $everyserverstcount++ + $trigger = $tr.Name + + Write-Message -Level Verbose -Message "Looking in trigger $trigger for textBody with pattern $pattern in object $($tr.Parent) at database $db" + if ($tr.TextBody -match $Pattern) { + + $triggerText = $tr.TextBody.split("`n`r") + $trTextFound = $triggerText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + TriggerLevel = "Object" + Database = $db.name + Object = $tr.Parent + Name = $tr.Name + IsSystemObject = $tr.IsSystemObject + CreateDate = $tr.CreateDate + LastModified = $tr.DateLastModified + TriggerTextFound = $trTextFound -join "`n" + Trigger = $tr + TriggerFullText = $tr.TextBody + } | Select-DefaultView -ExcludeProperty Trigger, TriggerFullText + } + } + } + } + Write-Message -Level Verbose -Message "Evaluated $triggercount triggers in $db" + } + } + Write-Message -Level Verbose -Message "Evaluated $totalcount total triggers in $dbcount databases" + } + } + end { + Write-Message -Level Verbose -Message "Evaluated $everyserverstcount total triggers" + } +} +function Find-DbaUnusedIndex { + <# + .SYNOPSIS + Find Unused indexes + + .DESCRIPTION + This command will help you to find Unused indexes on a database or a list of databases + + Also tells how much space you can save by dropping the index. + We show the type of compression so you can make a more considered decision. + For now only supported for CLUSTERED and NONCLUSTERED indexes + + You can select the indexes you want to drop on the gridview and by clicking OK the drop statement will be generated. + + .PARAMETER SqlInstance + The SQL Server you want to check for unused indexes. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER FilePath + Specifies the path of a file to write the DROP statements to. + + .PARAMETER NoClobber + If this switch is enabled, the output file will not be overwritten. + + .PARAMETER Append + If this switch is enabled, content will be appended to the output file. + + .PARAMETER IgnoreUptime + Less than 7 days uptime can mean that analysis of unused indexes is unreliable, and normally no results will be returned. By setting this option results will be returned even if the Instance has been running for less that 7 days. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Index + Author: Aaron Nelson (@SQLvariant), SQLvariant.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaUnusedIndex + + .EXAMPLE + Find-DbaUnusedIndex -SqlInstance sql2005 -FilePath C:\temp\sql2005-UnusedIndexes.sql + + Generates the SQL statements to drop the selected unused indexes on server "sql2005". The statements are written to the file "C:\temp\sql2005-UnusedIndexes.sql" + + .EXAMPLE + Find-DbaUnusedIndex -SqlInstance sql2005 -FilePath C:\temp\sql2005-UnusedIndexes.sql -Append + + Generates the SQL statements to drop the selected unused indexes on server "sql2005". The statements are written to the file "C:\temp\sql2005-UnusedIndexes.sql", appending if the file already exists. + + .EXAMPLE + Find-DbaUnusedIndex -SqlInstance sqlserver2016 -SqlCredential $cred + + Generates the SQL statements to drop the selected unused indexes on server "sqlserver2016", using SQL Authentication to connect to the database. + + .EXAMPLE + Find-DbaUnusedIndex -SqlInstance sqlserver2016 -Database db1, db2 + + Generates the SQL Statement to to drop selected indexes in databases db1 & db2 on server "sqlserver2016". + + .EXAMPLE + Find-DbaUnusedIndex -SqlInstance sqlserver2016 + + Generates the SQL statements to drop selected indexes on all user databases. + + .EXAMPLE + Fine-DbaUnusedIndex -SqlInstance sqlserver2016 -IgnoreUptime + + Generates the SQL statements to drop selected indexes on all user databases even if the instance has been online for less than 7 days. + Note that results may not have enough detail for all indexes, so care should be taken when using them or the generated scripts. Best practice is to allow a full week to capture the mmajority of index use cases + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias("OutFile", "Path")] + [string]$FilePath, + [switch]$NoClobber, + [switch]$Append, + [switch]$IgnoreUptime, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + + # Support Compression 2008+ + $unusedQuery = " + SELECT DB_NAME(database_id) AS 'DatabaseName' + ,s.name AS 'SchemaName' + ,t.name AS 'TableName' + ,i.object_id AS ObjectId + ,i.name AS 'IndexName' + ,i.index_id as 'IndexId' + ,i.type_desc as 'TypeDesc' + ,user_seeks as 'UserSeeks' + ,user_scans as 'UserScans' + ,user_lookups as 'UserLookups' + ,user_updates as 'UserUpdates' + ,last_user_seek as 'LastUserSeek' + ,last_user_scan as 'LastUserScan' + ,last_user_lookup as 'LastUserLookup' + ,last_user_UPDATE as 'LastUserUpdate' + ,system_seeks as 'SystemSeeks' + ,system_scans as 'SystemScans' + ,system_lookups as 'SystemLookup' + ,system_updates as 'SystemUpdates' + ,last_system_seek as 'LastSystemSeek' + ,last_system_scan as 'LastSystemScan' + ,last_system_lookup as 'LastSystemLookup' + ,last_system_update as 'LastSystemUpdate' + FROM SYS.TABLES T + JOIN SYS.SCHEMAS S + ON T.schema_id = s.schema_id + JOIN SYS.indexes i + ON i.object_id = t.object_id LEFT OUTER + JOIN sys.dm_db_index_usage_stats iu + ON iu.object_id = i.object_id + AND iu.index_id = i.index_id + WHERE iu.database_id = DB_ID() + AND OBJECTPROPERTY(i.[object_id], 'IsMSShipped') = 0 + AND user_seeks = 0 + AND user_scans = 0 + AND user_lookups = 0 + AND i.type_desc NOT IN ('HEAP', 'CLUSTERED COLUMNSTORE')" + + if ($FilePath.Length -gt 0) { + if ($FilePath -notlike "*\*") { + $FilePath = ".\$FilePath" + } + $directory = Split-Path $FilePath + $exists = Test-Path $directory + + if ($exists -eq $false) { + Stop-Function -Message "Parent directory $directory does not exist." + return + } + } + + Write-Message -Level Output -Message "Attempting to connect to Sql Server." + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + process { + if (Test-FunctionInterrupt) { return } + + if ($server.VersionMajor -lt 9) { + Stop-Function -Message "This function does not support versions lower than SQL Server 2005 (v9)." + return + } + + $lastRestart = $server.Databases['tempdb'].CreateDate + $endDate = Get-Date -Date $lastRestart + $diffDays = (New-TimeSpan -Start $endDate -End (Get-Date)).Days + + if ($diffDays -le 6) { + if ($IgnoreUptime -ne $true) { + Stop-Function -Message "The SQL Service was restarted on $lastRestart, which is not long enough for a solid evaluation." + return + } + else { + Write-Message -Level Warning -Message "The SQL Service was restarted on $lastRestart, which is not long enough for a solid evaluation." + } + } + + <# + Validate if server version is: + - sql 2012 and if have SP3 CU3 (Build 6537) or higher + - sql 2014 and if have SP2 (Build 5000) or higher + If the major version is the same but the build is lower, throws the message + #> + if ( + ($server.VersionMajor -eq 11 -and $server.BuildNumber -lt 6537) ` + -or ($server.VersionMajor -eq 12 -and $server.BuildNumber -lt 5000) + ) { + Stop-Function -Message "This SQL version has a known issue. Rebuilding an index clears any existing row entry from sys.dm_db_index_usage_stats for that index.`r`nPlease refer to connect item: https://support.microsoft.com/en-us/help/3160407/fix-sys-dm-db-index-usage-stats-missing-information-after-index-rebuil" + return + } + + if ($diffDays -le 33) { + Write-Message -Level Warning -Message "The SQL Service was restarted on $lastRestart, which may not be long enough for a solid evaluation." + } + + if ($pipedatabase.Length -gt 0) { + $database = $pipedatabase.name + } + + if ($database.Count -eq 0) { + $database = ($server.Databases | Where-Object { $_.IsSystemObject -eq 0 -and $_.IsAccessible }).Name + } + + if ($database.Count -gt 0) { + foreach ($db in $database) { + if ($ExcludeDatabase -contains $db -or $null -eq $server.Databases[$db]) { + continue + } + if ($server.Databases[$db].IsAccessible -eq $false) { + Write-Message -Level Warning -Message "Database [$db] is not accessible." + continue + } + try { + Write-Message -Level Output -Message "Getting indexes from database '$db'." + + $sql = $unusedQuery + + $unusedIndex = $server.Databases[$db].ExecuteWithResults($sql) + + $scriptGenerated = $false + + if ($unusedIndex.Tables[0].Rows.Count -gt 0) { + $indexesToDrop = $unusedIndex.Tables[0] + + if ($indexesToDrop.Count -gt 0 -or !([string]::IsNullOrEmpty($indexesToDrop))) { + + foreach ($index in $indexesToDrop) { + if ($FilePath.Length -gt 0) { + Write-Message -Level Output -Message "Exporting $($index.TableName).$($index.IndexName)" + $sqlout += "USE [$($index.DatabaseName)]`r`n" + $sqlout += "GO`r`n" + $sqlout += "IF EXISTS (SELECT 1 FROM sys.indexes WHERE [object_id] = OBJECT_ID('$($index.SchemaName).$($index.TableName)') AND name = '$($index.IndexName)')`r`n" + $sqlout += "DROP INDEX $($index.SchemaName).$($index.TableName).$($index.IndexName)`r`n" + $sqlout += "GO`r`n`r`n"` + + } + } + + if ($FilePath.Length -gt 0) { + $sqlout | Out-File -FilePath $FilePath -Append:$Append -NoClobber:$NoClobber + } + else { + $indexesToDrop + } + + $scriptGenerated = $true + } + } + else { + Write-Message -Level Output -Message "No Unused indexes found!" + } + } + catch { + Stop-Function -Message "Issue gathering indexes" -Category InvalidOperation -ErrorRecord $_ -Target $db + } + } + + if ($scriptGenerated) { + Write-Message -Level Warning -Message "Confirm the generated script before execute!" + } + if ($FilePath.Length -gt 0) { + Write-Message -Level Output -Message "Script generated to $FilePath" + } + } + else { + Write-Message -Level Output -Message "There are no databases to analyse." + } + } + end { + if (Test-FunctionInterrupt) { + return + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-SqlUnusedIndex + } +} +#ValidationTags#Messaging# +function Find-DbaUserObject { + <# + .SYNOPSIS + Searches SQL Server to find user-owned objects (ie. not dbo or sa) or for any object owned by a specific user specified by the Pattern parameter. + + .DESCRIPTION + Looks at the below list of objects to see if they are either owned by a user or a specific user (using the parameter -Pattern) + Database Owner + Agent Job Owner + Used in Credential + USed in Proxy + SQL Agent Steps using a Proxy + Endpoints + Server Roles + Database Schemas + Database Roles + Database Assembles + Database Synonyms + + .PARAMETER SqlInstance + SqlInstance name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Pattern + The regex pattern that the command will search for + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Object + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaUserObject + + .EXAMPLE + Find-DbaUserObject -SqlInstance DEV01 -Pattern ad\stephen + + Searches user objects for owner ad\stephen + + .EXAMPLE + Find-DbaUserObject -SqlInstance DEV01 -Verbose + + Shows all user owned (non-sa, non-dbo) objects and verbose output + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlInstances")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Pattern, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ($Pattern -match '^[\w\d\.-]+\\[\w\d\.-]+$') { + Write-Message -Level Verbose -Message "Too few slashes, adding extra as required by regex" + $Pattern = $Pattern.Replace('\', '\\') + } + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $saname = Get-SaLoginName $server + + ## Credentials + if (-not $pattern) { + Write-Message -Level Verbose -Message "Gathering data on instance objects" + $creds = $server.Credentials + $proxies = $server.JobServer.ProxyAccounts + $endPoints = $server.Endpoints | Where-Object { $_.Owner -ne $saname } + + Write-Message -Level Verbose -Message "Gather data on Agent Jobs ownership" + $jobs = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $saname } + } + else { + Write-Message -Level Verbose -Message "Gathering data on instance objects" + $creds = $server.Credentials | Where-Object { $_.Identity -match $pattern } + $proxies = $server.JobServer.ProxyAccounts | Where-Object { $_.CredentialIdentity -match $pattern } + $endPoints = $server.Endpoints | Where-Object { $_.Owner -match $pattern } + + Write-Message -Level Verbose -Message "Gather data on Agent Jobs ownership" + $jobs = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -match $pattern } + } + + ## dbs + if (-not $pattern) { + foreach ($db in $server.Databases | Where-Object { $_.Owner -ne $saname }) { + Write-Message -Level Verbose -Message "checking if $db is owned " + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Database" + Owner = $db.Owner + Name = $db.Name + Parent = $db.Parent.Name + } + } + } + else { + foreach ($db in $server.Databases | Where-Object { $_.Owner -match $pattern }) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Database" + Owner = $db.Owner + Name = $db.Name + Parent = $db.Parent.Name + } + } + } + + ## agent jobs + if (-not $pattern) { + foreach ($job in $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $saname }) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Agent Job" + Owner = $job.OwnerLoginName + Name = $job.Name + Parent = $job.Parent.Name + } + } + } + else { + foreach ($job in $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -match $pattern }) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Agent Job" + Owner = $job.OwnerLoginName + Name = $job.Name + Parent = $job.Parent.Name + } + } + } + + ## credentials + foreach ($cred in $creds) { + ## list credentials using the account + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Credential" + Owner = $cred.Identity + Name = $cred.Name + Parent = $cred.Parent.Name + } + } + + ## proxies + foreach ($proxy in $proxies) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Proxy" + Owner = $proxy.CredentialIdentity + Name = $proxy.Name + Parent = $proxy.Parent.Name + } + + ## list agent jobs steps using proxy + foreach ($job in $server.JobServer.Jobs) { + foreach ($step in $job.JobSteps | Where-Object { $_.ProxyName -eq $proxy.Name }) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Agent Step" + Owner = $step.ProxyName + Name = $step.Name + Parent = $step.Parent.Name #$step.Name + } + } + } + } + + + ## endpoints + foreach ($endPoint in $endPoints) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Endpoint" + Owner = $endpoint.Owner + Name = $endPoint.Name + Parent = $endPoint.Parent.Name + } + } + + ## Server Roles + if (-not $pattern) { + foreach ($role in $server.Roles | Where-Object { $_.Owner -ne $saname }) { + Write-Message -Level Verbose -Message "checking if $db is owned " + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Server Role" + Owner = $role.Owner + Name = $role.Name + Parent = $role.Parent.Name + } + } + } + else { + foreach ($role in $server.Roles | Where-Object { $_.Owner -match $pattern }) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Server Role" + Owner = $role.Owner + Name = $role.Name + Parent = $role.Parent.Name + } + } + } + + ## Loop internal database + foreach ($db in $server.Databases | Where-Object IsAccessible) { + Write-Message -Level Verbose -Message "Gather user owned object in database: $db" + ##schemas + $sysSchemas = "DatabaseMailUserRole", "db_ssisadmin", "db_ssisltduser", "db_ssisoperator", "SQLAgentOperatorRole", "SQLAgentReaderRole", "SQLAgentUserRole", "TargetServersRole", "RSExecRole" + + if (-not $pattern) { + $schemas = $db.Schemas | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -ne "dbo" -and $sysSchemas -notcontains $_.Owner } + } + else { + $schemas = $db.Schemas | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -match $pattern -and $sysSchemas -notcontains $_.Owner } + } + foreach ($schema in $schemas) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Schema" + Owner = $schema.Owner + Name = $schema.Name + Parent = $schema.Parent.Name + } + } + + ## database roles + if (-not $pattern) { + $roles = $db.Roles | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -ne "dbo" } + } + else { + $roles = $db.Roles | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -match $pattern } + } + foreach ($role in $roles) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Database Role" + Owner = $role.Owner + Name = $role.Name + Parent = $role.Parent.Name + } + } + + ## assembly + if (-not $pattern) { + $assemblies = $db.Assemblies | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -ne "dbo" } + } + else { + $assemblies = $db.Assemblies | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -match $pattern } + } + + foreach ($assembly in $assemblies) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Database Assembly" + Owner = $assembly.Owner + Name = $assembly.Name + Parent = $assembly.Parent.Name + } + } + + ## synonyms + if (-not $pattern) { + $synonyms = $db.Synonyms | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -ne "dbo" } + } + else { + $synonyms = $db.Synonyms | Where-Object { $_.IsSystemObject -eq 0 -and $_.Owner -match $pattern } + } + + foreach ($synonym in $synonyms) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Type = "Database Synonyms" + Owner = $synonym.Owner + Name = $synonym.Name + Parent = $synonym.Parent.Name + } + } + } + } + } +} +function Find-DbaView { + <# + .SYNOPSIS + Returns all views that contain a specific case-insensitive string or regex pattern. + + .DESCRIPTION + This function can either run against specific databases or all databases searching all user or user and system views. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Pattern + String pattern that you want to search for in the view textbody + + .PARAMETER IncludeSystemObjects + By default, system views are ignored but you can include them within the search using this parameter. + + Warning - this will likely make it super slow if you run it on all databases. + + .PARAMETER IncludeSystemDatabases + By default system databases are ignored but you can include them within the search using this parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: View + Author: Cláudio Silva (@ClaudioESSilva) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Find-DbaView + + .EXAMPLE + Find-DbaView -SqlInstance DEV01 -Pattern whatever + + Searches all user databases views for "whatever" in the textbody + + .EXAMPLE + Find-DbaView -SqlInstance sql2016 -Pattern '\w+@\w+\.\w+' + + Searches all databases for all views that contain a valid email pattern in the textbody + + .EXAMPLE + Find-DbaView -SqlInstance DEV01 -Database MyDB -Pattern 'some string' -Verbose + + Searches in "mydb" database views for "some string" in the textbody + + .EXAMPLE + Find-DbaView -SqlInstance sql2016 -Database MyDB -Pattern RUNTIME -IncludeSystemObjects + + Searches in "mydb" database views for "runtime" in the textbody + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $true)] + [string]$Pattern, + [switch]$IncludeSystemObjects, + [switch]$IncludeSystemDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = "SELECT OBJECT_SCHEMA_NAME(vw.object_id) as ViewSchema, vw.name, m.definition as TextBody FROM sys.sql_modules m, sys.views vw WHERE m.object_id = vw.object_id" + if (!$IncludeSystemObjects) { $sql = "$sql AND vw.is_ms_shipped = 0" } + $everyservervwcount = 0 + } + process { + foreach ($Instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $Instance" + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $Instance" + continue + } + + if ($server.versionMajor -lt 9) { + Write-Message -Level Warning -Message "This command only supports SQL Server 2005 and above." + Continue + } + + if ($IncludeSystemDatabases) { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" } + } + else { + $dbs = $server.Databases | Where-Object { $_.Status -eq "normal" -and $_.IsSystemObject -eq $false } + } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + $totalcount = 0 + $dbcount = $dbs.count + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Searching on database $db" + + # If system objects aren't needed, find view text using SQL + # This prevents SMO from having to enumerate + + if (!$IncludeSystemObjects) { + Write-Message -Level Debug -Message $sql + $rows = $db.ExecuteWithResults($sql).Tables.Rows + $vwcount = 0 + + foreach ($row in $rows) { + $totalcount++; $vwcount++; $everyservervwcount++ + + $viewSchema = $row.ViewSchema + $view = $row.name + + Write-Message -Level Verbose -Message "Looking in View: $viewSchema.$view TextBody for $pattern" + if ($row.TextBody -match $Pattern) { + $vw = $db.Views | Where-Object {$_.Schema -eq $viewSchema -and $_.Name -eq $view} + + $viewText = $vw.TextBody.split("`n`r") + $vwTextFound = $viewText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + Database = $db.Name + Schema = $vw.Schema + Name = $vw.Name + Owner = $vw.Owner + IsSystemObject = $vw.IsSystemObject + CreateDate = $vw.CreateDate + LastModified = $vw.DateLastModified + ViewTextFound = $vwTextFound -join "`n" + View = $vw + ViewFullText = $vw.TextBody + } | Select-DefaultView -ExcludeProperty View, ViewFullText + } + } + } + else { + $Views = $db.Views + + foreach ($vw in $Views) { + $totalcount++; $vwcount++; $everyservervwcount++ + + $viewSchema = $row.ViewSchema + $view = $vw.Name + + Write-Message -Level Verbose -Message "Looking in View: $viewSchema.$view TextBody for $pattern" + if ($vw.TextBody -match $Pattern) { + + $viewText = $vw.TextBody.split("`n`r") + $vwTextFound = $viewText | Select-String -Pattern $Pattern | ForEach-Object { "(LineNumber: $($_.LineNumber)) $($_.ToString().Trim())" } + + [PSCustomObject]@{ + ComputerName = $server.NetName + SqlInstance = $server.ServiceName + Database = $db.Name + Schema = $vw.Schema + Name = $vw.Name + Owner = $vw.Owner + IsSystemObject = $vw.IsSystemObject + CreateDate = $vw.CreateDate + LastModified = $vw.DateLastModified + ViewTextFound = $vwTextFound -join "`n" + View = $vw + ViewFullText = $vw.TextBody + } | Select-DefaultView -ExcludeProperty View, ViewFullText + } + } + } + Write-Message -Level Verbose -Message "Evaluated $vwcount views in $db" + } + Write-Message -Level Verbose -Message "Evaluated $totalcount total views in $dbcount databases" + } + } + end { + Write-Message -Level Verbose -Message "Evaluated $everyservervwcount total views" + } +} +function Format-DbaBackupInformation { + <# + .SYNOPSIS + Transforms the data in a dbatools backuphistory object for a restore + + .DESCRIPTION + Performs various mapping on Backup History, ready restoring + Options include changing restore paths, backup paths, database name and many others + + .PARAMETER BackupHistory + A dbatools backupHistory object, normally this will have been created using Select-DbaBackupInformation + + .PARAMETER ReplaceDatabaseName + If a single value is provided, this will be replaced do all occurrences a database name + If a Hashtable is passed in, each database name mention will be replaced as specified. If a database's name does not appear it will not be replace + DatabaseName will also be replaced where it occurs in the file paths of data and log files. + Please note, that this won't change the Logical Names of datafiles, that has to be done with a separate Alter DB call + + .PARAMETER DatabaseNamePrefix + This string will be prefixed to all restored database's name + + .PARAMETER DataFileDirectory + This will move ALL restored files to this location during the restore + + .PARAMETER LogFileDirectory + This will move all log files to this location, overriding DataFileDirectory + + .PARAMETER DestinationFileStreamDirectory + This move the FileStream folder and contents to the new location, overriding DataFileDirectory + + .PARAMETER FileNamePrefix + This string will be prefixed to all restored files (Data and Log) + + .PARAMETER RebaseBackupFolder + Use this to rebase where your backups are stored. + + .PARAMETER Continue + Indicates that this is a continuing restore + + .PARAMETER DatabaseFilePrefix + A string that will be prefixed to every file restored + + .PARAMETER DatabaseFileSuffix + A string that will be suffixed to every file restored + + .PARAMETER ReplaceDbNameInFile + If set, will replace the old databasename with the new name if it occurs in the file name + + .PARAMETER FileMapping + A hashtable that can be used to move specific files to a location. + $FileMapping = @{'DataFile1'='c:\restoredfiles\Datafile1.mdf';'DataFile3'='d:\DataFile3.mdf'} + And files not specified in the mapping will be restored to their original location + This Parameter is exclusive with DestinationDataDirectory + If specified, this will override any other file renaming/relocation options. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Author:Stuart Moore (@napalmgram stuart-moore.com ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Format-DbaBackupInformation + + .EXAMPLE + $History | Format-DbaBackupInformation -ReplaceDatabaseName NewDb + + Changes as databasename references to NewDb, both in the database name and any restore paths. Note, this will fail if the BackupHistory object contains backups for more than 1 database + + .EXAMPLE + $History | Format-DbaBackupInformation -ReplaceDatabaseName @{'OldB'='NewDb';'ProdHr'='DevHr'} + + Will change all occurences of original database name in the backup history (names and restore paths) using the mapping in the hashtable. + In this example any occurance of OldDb will be replaced with NewDb and ProdHr with DevPR + + .EXAMPLE + $History | Format-DbaBackupInformation -DataFileDirectory 'D:\DataFiles\' -LogFileDirectory 'E:\LogFiles\ + + This example with change the restore path for all datafiles (everything that is not a log file) to d:\datafiles + And all Transaction Log files will be restored to E:\Logfiles + + .EXAMPLE + $History | Formate-DbaBackupInformation -RebaseBackupFolder f:\backups + + This example changes the location that SQL Server will look for the backups. This is useful if you've moved the backups to a different location + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$BackupHistory, + [object]$ReplaceDatabaseName, + [switch]$ReplaceDbNameInFile, + [string]$DataFileDirectory, + [string]$LogFileDirectory, + [string]$DestinationFileStreamDirectory, + [string]$DatabaseNamePrefix, + [string]$DatabaseFilePrefix, + [string]$DatabaseFileSuffix, + [string]$RebaseBackupFolder, + [switch]$Continue, + [hashtable]$FileMapping, + [switch]$EnableException + ) + Begin { + + Write-Message -Message "Starting" -Level Verbose + if ($null -ne $ReplaceDatabaseName) { + if ($ReplaceDatabaseName -is [string] -or $ReplaceDatabaseName.ToString() -ne 'System.Collections.Hashtable') { + Write-Message -Message "String passed in for DB rename" -Level Verbose + $ReplaceDatabaseNameType = 'single' + } + elseif ($ReplaceDatabaseName -is [HashTable] -or $ReplaceDatabaseName.ToString() -eq 'System.Collections.Hashtable' ) { + Write-Message -Message "Hashtable passed in for DB rename" -Level Verbose + $ReplaceDatabaseNameType = 'multi' + } + else { + Write-Message -Message "ReplacemenDatabaseName is $($ReplaceDatabaseName.Gettype().ToString()) - $ReplaceDatabaseName" -level Verbose + } + } + if ((Test-Bound -Parameter DataFileDirectory) -and $DataFileDirectory[-1] -eq '\' ) { + $DataFileDirectory = $DataFileDirectory.substring(0, $DataFileDirectory.length - 1) + } + if ((Test-Bound -Parameter DestinationFileStreamDirectory) -and $DestinationFileStreamDirectory[-1] -eq '\' ) { + $DestinationFileStreamDirectory = $DestinationFileStreamDirectory.substring(0, $DestinationFileStreamDirectory.length - 1) + } + if ((Test-Bound -Parameter LogFileDirectory) -and $LogFileDirectory[-1] -eq '\' ) { + $LogFileDirectory = $LogFileDirectory.substring(0, $LogFileDirectory.length - 1) + } + if ((Test-Bound -Parameter RebaseBackupFolder) -and $RebaseBackupFolder[-1] -eq '\' ) { + $RebaseBackupFolder = $RebaseBackupFolder.substring(0, $RebaseBackupFolder.length - 1) + } + } + + + Process { + + ForEach ($History in $BackupHistory) { + if ("OriginalDatabase" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'OriginalDatabase' -Type NoteProperty -Value $History.Database + } + if ("OriginalFileList" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'OriginalFileList' -Type NoteProperty -Value '' + $History | ForEach-Object {$_.OriginalFileList = $_.FileList} + } + if ("OriginalFullName" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'OriginalFullName' -Type NoteProperty -Value $History.FullName + } + if ("IsVerified" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'IsVerified' -Type NoteProperty -Value $False + } + Switch ($History.Type) { + 'Full' {$History.Type = 'Database'} + 'Differential' {$History.Type = 'Database Differential'} + 'Log' {$History.Type = 'Transaction Log'} + } + + + if ($ReplaceDatabaseNameType -eq 'single' -and $ReplaceDatabaseName -ne '' ) { + $History.Database = $ReplaceDatabaseName + $ReplaceMentName = $ReplaceDatabaseName + Write-Message -Message "New DbName (String) = $($History.Database)" -Level Verbose + } + elseif ($ReplaceDatabaseNameType -eq 'multi') { + if ($null -ne $ReplaceDatabaseName[$History.Database]) { + $History.Database = $ReplaceDatabaseName[$History.Database] + $ReplacementName = $ReplaceDatabaseName[$History.Database] + Write-Message -Message "New DbName (Hash) = $($History.Database)" -Level Verbose + } + } + $History.Database = $DatabaseNamePrefix + $History.Database + if ($true -ne $Continue) { + $History.FileList | ForEach-Object { + if ($null -ne $FileMapping ) { + if ($null -ne $FileMapping[$_.LogicalName]) { + $_.PhysicalName = $FileMapping[$_.LogicalName] + } + } + else { + if ($ReplaceDbNameInFile -eq $true) { + $_.PhysicalName = $_.PhysicalName -Replace $History.OriginalDatabase, $History.Database + } + Write-message -Message " 1 PhysicalName = $($_.PhysicalName) " -Level Verbose + $Pname = [System.Io.FileInfo]$_.PhysicalName + $RestoreDir = $Pname.DirectoryName + if ($_.Type -eq 'D' -or $_.FileType -eq 'D') { + if ('' -ne $DataFileDirectory) { + $RestoreDir = $DataFileDirectory + } + } + elseif ($_.Type -eq 'L' -or $_.FileType -eq 'L') { + if ('' -ne $LogFileDirectory) { + $RestoreDir = $LogFileDirectory + } + elseif ('' -ne $DataFileDirectory) { + $RestoreDir = $DataFileDirectory + } + } + elseif ($_.Type -eq 'S' -or $_.FileType -eq 'S') { + if ('' -ne $DestinationFileStreamDirectory) { + $RestoreDir = $DestinationFileStreamDirectory + } + elseif ('' -ne $DataFileDirectory) { + $RestoreDir = $DataFileDirectory + } + } + + $_.PhysicalName = $RestoreDir + "\" + $DatabaseFilePrefix + $Pname.BaseName + $DatabaseFileSuffix + $pname.extension + Write-message -Message "PhysicalName = $($_.PhysicalName) " -Level Verbose + } + } + } + if ($null -ne $RebaseBackupFolder -and $History.FullName[0] -notmatch 'http') { + $History.FullName | ForEach-Object { + $file = [System.IO.FileInfo]$_ + $_ = $RebaseBackupFolder + "\" + $file.BaseName + $file.Extension + } + } + $History + } + } +} +function Get-DbaAgDatabase { + <# + .SYNOPSIS + Outputs the databases involved in the Availability Group(s) found on the server. + + .DESCRIPTION + Default view provides most common set of properties for information on the database in an Availability Group(s). + + Information returned on the database will be specific to that replica, whether it is primary or a secondary. + + This command will return an SMO object, but it is the AvailabilityDatabases object and not the Server.Databases object. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2012 or higher. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). + + .PARAMETER AvailabilityGroup + Specify the Availability Group name that you want to get information on. + + .PARAMETER Database + Specify the database(s) to pull information for. This list is auto-populated from the server for tab completion. Multiple databases can be specified. If none are specified all databases will be processed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup, Replica + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgDatabase + + .EXAMPLE + Get-DbaAgDatabase -SqlInstance sqlserver2014a + + Returns basic information on all the databases in each Availability Group found on sqlserver2014a + + .EXAMPLE + Get-DbaAgDatabase -SqlInstance sqlserver2014a -AvailabilityGroup AG-a + + Returns basic information on all the databases in the Availability Group AG-a on sqlserver2014a + + .EXAMPLE + Get-DbaAgDatabase -SqlInstance sqlserver2014a -AvailabilityGroup AG-a -Database AG-Database + + Returns basic information on the database AG-Database found in the Availability Group AG-a on server sqlserver2014a + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ValueFromPipeline = $true)] + [object[]]$AvailabilityGroup, + [object[]]$Database, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($serverName in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $serverName -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.IsHadrEnabled -eq $false) { + Stop-Function -Message "Availability Group (HADR) is not configured for the instance: $serverName." -Target $serverName -Continue + } + + $ags = $server.AvailabilityGroups + if ($AvailabilityGroup) { + $ags = $ags | Where-Object Name -in $AvailabilityGroup + } + + foreach ($ag in $ags) { + $agDatabases = $ag.AvailabilityDatabases + foreach ($agDb in $agDatabases) { + if ($Database -and $agDb.Name -notmatch $Database) { + continue + } + + Add-Member -Force -InputObject $agDb -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $agDb -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $agDb -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $agDb -MemberType NoteProperty -Name Replica -value $server.NetName + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Parent as AvailabilityGroup', 'Replica', 'Name as DatabaseName', 'SynchronizationState', 'IsFailoverReady', 'IsJoined', 'IsSuspended' + Select-DefaultView -InputObject $agDb -Property $defaults + } + } + } + } +} +function Get-DbaAgentAlert { + <# + .SYNOPSIS + Returns all SQL Agent alerts on a SQL Server Agent. + + .DESCRIPTION + This function returns SQL Agent alerts. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Agent, SMO + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .LINK + https://dbatools.io/Get-DbaAgentAlert + + .EXAMPLE + Get-DbaAgentAlert -SqlInstance ServerA,ServerB\instanceB + Returns all SQL Agent alerts on serverA and serverB\instanceB + + .EXAMPLE + 'serverA','serverB\instanceB' | Get-DbaAgentAlert + Returns all SQL Agent alerts on serverA and serverB\instanceB + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "Instance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [Alias('Silent')] + [switch]$EnableException + + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Verbose -Message "Getting Edition from $server" + Write-Message -Level Verbose -Message "$server is a $($server.Edition)" + + if ($server.Edition -like 'Express*') { + Stop-Function -Message "There is no SQL Agent on $server, it's a $($server.Edition)" -Continue + } + + $defaults = "ComputerName", "SqlInstance", "InstanceName", "Name", "ID", "JobName", "AlertType", "CategoryName", "Severity", "IsEnabled", "DelayBetweenResponses", "LastRaised", "OccurrenceCount" + + $alerts = $server.Jobserver.Alerts + + foreach ($alert in $alerts) { + $lastraised = [dbadatetime]$alert.LastOccurrenceDate + + Add-Member -Force -InputObject $alert -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $alert -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $alert -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $alert -MemberType NoteProperty Notifications -value $alert.EnumNotifications() + Add-Member -Force -InputObject $alert -MemberType NoteProperty LastRaised -value $lastraised + + Select-DefaultView -InputObject $alert -Property $defaults + } + } + } +} +#ValidationTags#Messaging# +function Get-DbaAgentJob { + <# + .SYNOPSIS + Gets SQL Agent Job information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaAgentJob returns connected SMO object for SQL Agent Job information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The job(s) to process - this list is auto-populated from the server. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + The job(s) to exclude - this list is auto-populated from the server. + + .PARAMETER NoDisabledJobs + Switch will exclude disabled jobs from the output. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Job, Agent + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentJob + + .EXAMPLE + Get-DbaAgentJob -SqlInstance localhost + + Returns all SQL Agent Jobs on the local default SQL Server instance + + .EXAMPLE + Get-DbaAgentJob -SqlInstance localhost, sql2016 + + Returns all SQl Agent Jobs for the local and sql2016 SQL Server instances + + .EXAMPLE + Get-DbaAgentJob -SqlInstance localhost -Job BackupData, BackupDiff + + Returns all SQL Agent Jobs named BackupData and BackupDiff from the local SQL Server instance. + + .EXAMPLE + Get-DbaAgentJob -SqlInstance localhost -ExcludeJob BackupDiff + + Returns all SQl Agent Jobs for the local SQL Server instances, except the BackupDiff Job. + + .EXAMPLE + Get-DbaAgentJob -SqlInstance localhost -NoDisabledJobs + + Returns all SQl Agent Jobs for the local SQL Server instances, excluding the disabled jobs. + + .EXAMPLE + $servers | Get-DbaAgentJob | Out-GridView -Passthru | Start-DbaAgentJob -WhatIf + + Find all of your Jobs from servers in the $server collection, select the jobs you want to start then see jobs would start if you ran Start-DbaAgentJob + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Job, + [object[]]$ExcludeJob, + [switch]$NoDisabledJobs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $jobs = $server.JobServer.Jobs + + if ($Job) { + $jobs = $jobs | Where-Object Name -In $Job + } + if ($ExcludeJob) { + $jobs = $jobs | Where-Object Name -NotIn $ExcludeJob + } + if ($NoDisabledJobs) { + $jobs = $Jobs | Where-Object IsEnabled -eq $true + } + + foreach ($agentJob in $jobs) { + Add-Member -Force -InputObject $agentJob -MemberType NoteProperty -Name ComputerName -value $agentJob.Parent.Parent.NetName + Add-Member -Force -InputObject $agentJob -MemberType NoteProperty -Name InstanceName -value $agentJob.Parent.Parent.ServiceName + Add-Member -Force -InputObject $agentJob -MemberType NoteProperty -Name SqlInstance -value $agentJob.Parent.Parent.DomainInstanceName + + Select-DefaultView -InputObject $agentJob -Property ComputerName, InstanceName, SqlInstance, Name, Category, OwnerLoginName, CurrentRunStatus, CurrentRunRetryAttempt, 'IsEnabled as Enabled', LastRunDate, LastRunOutcome, DateCreated, HasSchedule, OperatorToEmail, 'DateCreated as CreateDate' + } + } + } +} +function Get-DbaAgentJobCategory { + <# + .SYNOPSIS + Get-DbaAgentJobCategory retrieves the job categories. + + .DESCRIPTION + Get-DbaAgentJobCategory makes it possible to retrieve the job categories. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Category + The name of the category to filter out. If no category is used all catgories will be returned. + + .PARAMETER CategoryType + The type of category. This can be "LocalJob", "MultiServerJob" or "None". + If no category is used all catgories types will be returned. + + .PARAMETER Force + The force parameter will ignore some errors in the parameters and assume defaults. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, JobCategory + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentJobCategory + + .EXAMPLE + Get-DbaAgentJobCategory -SqlInstance sql1 + + Return all the job categories. + + .EXAMPLE + Get-DbaAgentJobCategory -SqlInstance sql1 -Category 'Log Shipping' + + Return all the job categories that have the name 'Log Shipping'. + + .EXAMPLE + Get-DbaAgentJobCategory -SqlInstance sstad-pc -CategoryType MultiServerJob + + Return all the job categories that have a type MultiServerJob. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [ValidateSet("LocalJob", "MultiServerJob", "None")] + [string]$CategoryType, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance." + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # get all the job categories + $jobCategories = $server.JobServer.JobCategories | + Where-Object { + ($_.Name -in $Category -or !$Category) -and + ($_.CategoryType -in $CategoryType -or !$CategoryType) + } + + # Set the default output + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name', 'ID', 'CategoryType', 'JobCount' + + # Loop through each of the categories + try { + foreach ($cat in $jobCategories) { + + # Get the jobs associated with the category + $jobCount = ($server.JobServer.Jobs | Where-Object {$_.CategoryID -eq $cat.ID}).Count + + # Add new properties to the category object + Add-Member -Force -InputObject $cat -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $cat -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $cat -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $cat -MemberType NoteProperty -Name JobCount -Value $jobCount + + # Show the result + Select-DefaultView -InputObject $cat -Property $defaults + } + } + catch { + Stop-Function -ErrorRecord $_ -Target $instance -Message "Failure. Collection may have been modified" -Continue + } + + } # for each instance + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished retrieving job category." -Level Verbose + } + +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaAgentJobHistory { + <# + .SYNOPSIS + Gets execution history of SQL Agent Job on instance(s) of SQL Server. + + .DESCRIPTION + Get-DbaAgentJobHistory returns all information on the executions still available on each instance(s) of SQL Server submitted. + The cleanup of SQL Agent history determines how many records are kept. + + https://msdn.microsoft.com/en-us/library/ms201680.aspx + https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.agent.jobhistoryfilter(v=sql.120).aspx + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job from which the history is wanted. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + The job(s) to exclude - this list is auto-populated from the server + + .PARAMETER StartDate + The DateTime starting from which the history is wanted. If unspecified, all available records will be processed. + + .PARAMETER EndDate + The DateTime before which the history is wanted. If unspecified, all available records will be processed. + + .PARAMETER NoJobSteps + Use this switch to discard all job steps, and return only the job totals + + .PARAMETER WithOutputFile + Use this switch to retrieve the output file (only if you want step details). Bonus points, we handle the quirks + of SQL Agent tokens to the best of our knowledge (https://technet.microsoft.com/it-it/library/ms175575(v=sql.110).aspx) + + .PARAMETER JobCollection + An array of SMO jobs + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Job, Agent + Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + Editor: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentJobHistory + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance localhost + + Returns all SQL Agent Job execution results on the local default SQL Server instance. + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance localhost, sql2016 + + Returns all SQL Agent Job execution results for the local and sql2016 SQL Server instances. + + .EXAMPLE + 'sql1','sql2\Inst2K17' | Get-DbaAgentJobHistory + + Returns all SQL Agent Job execution results for sql1 and sql2\Inst2K17. + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance sql2\Inst2K17 | select * + + Returns all properties for all SQl Agent Job execution results on sql2\Inst2K17. + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance sql2\Inst2K17 -Job 'Output File Cleanup' + + Returns all properties for all SQl Agent Job execution results of the 'Output File Cleanup' job on sql2\Inst2K17. + + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance sql2\Inst2K17 -Job 'Output File Cleanup' -WithOutputFile + + Returns all properties for all SQl Agent Job execution results of the 'Output File Cleanup' job on sql2\Inst2K17, + with additional properties that show the output filename path + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance sql2\Inst2K17 -NoJobSteps + + Returns the SQL Agent Job execution results for the whole jobs on sql2\Inst2K17, leaving out job step execution results. + + .EXAMPLE + Get-DbaAgentJobHistory -SqlInstance sql2\Inst2K17 -StartDate '2017-05-22' -EndDate '2017-05-23 12:30:00' + + Returns the SQL Agent Job execution results between 2017/05/22 00:00:00 and 2017/05/23 12:30:00 on sql2\Inst2K17. + + .EXAMPLE + Get-DbaAgentJob -SqlInstance sql2016 | Where Name -match backup | Get-DbaAgentJobHistory + + Gets all jobs with the name that match the regex pattern "backup" and then gets the job history from those. You can also use -Like *backup* in this example. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Server")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [object[]]$Job, + [object[]]$ExcludeJob, + [DateTime]$StartDate = "1900-01-01", + [DateTime]$EndDate = $(Get-Date), + [switch]$NoJobSteps, + [switch]$WithOutputFile, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Collection")] + [Microsoft.SqlServer.Management.Smo.Agent.Job]$JobCollection, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $filter = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobHistoryFilter + $filter.StartRunDate = $StartDate + $filter.EndRunDate = $EndDate + + + if ($NoJobSteps -and $WithOutputFile) { + Stop-Function -Message "You can't use -NoJobSteps and -WithOutputFile together" + } + + function Get-JobHistory { + [CmdletBinding()] + param ( + $Server, + $Job, + [switch]$WithOutputFile + ) + $tokenrex = [regex]'\$\((?[^()]+)\((?[^)]+)\)\)|\$\((?[^)]+)\)' + $propmap = @{ + 'INST' = $Server.ServiceName + 'MACH' = $Server.NetName + 'SQLDIR' = $Server.InstallDataDirectory + 'SQLLOGDIR' = $Server.ErrorLogPath + #'STEPCT' loop number ? + 'SRVR' = $Server.DomainInstanceName + # WMI( property ) impossible + } + + + $squote_rex = [regex]"(? + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, HelpMessage = 'The SQL Server Instance', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + Position = 0)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false, HelpMessage = 'SQL Credential', + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + Position = 1)] + [PSCredential]$SqlCredential, + [object[]]$Job, + [object[]]$ExcludeJob, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $sqlinstance) { + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $jobs = $Server.JobServer.Jobs + if ($Job) { + $jobs = $jobs | Where-Object Name -In $Job + } + if ($ExcludeJob) { + $jobs = $jobs | Where-Object Name -NotIn $ExcludeJob + } + foreach ($j in $Jobs) { + foreach ($Step in $j.JobSteps) { + if ($Step.OutputFileName) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Job = $j.Name + JobStep = $Step.Name + OutputFileName = $Step.OutputFileName + RemoteOutputFileName = Join-AdminUNC $Server.NetName $Step.OutputFileName + StepId = $Step.Id + } | Select-DefaultView -ExcludeProperty StepId + } + else { + Write-Message -Level Verbose -Message "$step for $j has no output file" + } + } + } + } + } +} +function Get-DbaAgentJobStep { + <# + .SYNOPSIS + Gets SQL Agent Job Step information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaAgentJobStep returns connected SMO object for SQL Agent Job Step for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The job(s) to process - this list is auto-populated from the server. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + The job(s) to exclude - this list is auto-populated from the server. + + .PARAMETER NoDisabledJobs + Switch will exclude disabled jobs from the output. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Job, Agent + Author: Klaas Vandenberghe (@PowerDbaKlaas), http://powerdba.eu + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentJobStep + + .EXAMPLE + Get-DbaAgentJobStep -SqlInstance localhost + + Returns all SQL Agent Job Steps on the local default SQL Server instance + + .EXAMPLE + Get-DbaAgentJobStep -SqlInstance localhost, sql2016 + + Returns all SQl Agent Job Steps for the local and sql2016 SQL Server instances + + .EXAMPLE + Get-DbaAgentJobStep -SqlInstance localhost -Job BackupData, BackupDiff + + Returns all SQL Agent Job Steps for the jobs named BackupData and BackupDiff from the local SQL Server instance. + + .EXAMPLE + Get-DbaAgentJobStep -SqlInstance localhost -ExcludeJob BackupDiff + + Returns all SQl Agent Job Steps for the local SQL Server instances, except for the BackupDiff Job. + + .EXAMPLE + Get-DbaAgentJobStep -SqlInstance localhost -NoDisabledJobs + + Returns all SQl Agent Job Steps for the local SQL Server instances, excluding the disabled jobs. + + .EXAMPLE + $servers | Get-DbaAgentJobStep + + Find all of your Job Steps from servers in the $server collection + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [object[]]$Job, + [object[]]$ExcludeJob, + [switch]$NoDisabledJobs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + Write-Message -Level Verbose -Message "Collecting jobs on $instance" + $jobs = $server.JobServer.Jobs + + if ($Job) { + $jobs = $jobs | Where-Object Name -In $Job + } + if ($ExcludeJob) { + $jobs = $jobs | Where-Object Name -NotIn $ExcludeJob + } + if ($NoDisabledJobs) { + $jobs = $Jobs | Where-Object IsEnabled -eq $true + } + Write-Message -Level Verbose -Message "Collecting job steps on $instance" + foreach ($agentJobStep in $jobs.jobsteps) { + Add-Member -Force -InputObject $agentJobStep -MemberType NoteProperty -Name ComputerName -value $agentJobStep.Parent.Parent.Parent.NetName + Add-Member -Force -InputObject $agentJobStep -MemberType NoteProperty -Name InstanceName -value $agentJobStep.Parent.Parent.Parent.ServiceName + Add-Member -Force -InputObject $agentJobStep -MemberType NoteProperty -Name SqlInstance -value $agentJobStep.Parent.Parent.Parent.DomainInstanceName + Add-Member -Force -InputObject $agentJobStep -MemberType NoteProperty -Name AgentJob -value $agentJobStep.Parent.Name + + Select-DefaultView -InputObject $agentJobStep -Property ComputerName, InstanceName, SqlInstance, AgentJob, Name, SubSystem, LastRunDate, LastRunOutcome, State + } + } + } +} +function Get-DbaAgentLog { + <# + .SYNOPSIS + Gets the "SQL Agent Error Log" of an instance + + .DESCRIPTION + Gets the "SQL Agent Error Log" of an instance. Returns all 10 error logs by default. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER LogNumber + An Int32 value that specifies the index number of the error log required. Error logs are listed 0 through 9 where 0 is the current error log and 9 is the oldest. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Logging + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentLog + + .EXAMPLE + Get-DbaAgentLog -SqlInstance sql01\sharepoint + + Returns the entire error log for the SQL Agent on sql01\sharepoint + + .EXAMPLE + Get-DbaAgentLog -SqlInstance sql01\sharepoint -LogNumber 3, 6 + + Returns log numbers 3 and 6 for the SQL Agent on sql01\sharepoint + + .EXAMPLE + $servers = "sql2014","sql2016", "sqlcluster\sharepoint" + $servers | Get-DbaAgentLog -LogNumber 0 + + Returns the most recent SQL Agent error logs for "sql2014","sql2016" and "sqlcluster\sharepoint" +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [ValidateRange(0, 9)] + [int[]]$LogNumber, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($LogNumber) { + foreach ($number in $lognumber) { + try { + foreach ($object in $server.JobServer.ReadErrorLog($number)) { + Write-Message -Level Verbose -Message "Processing $object" + Add-Member -Force -InputObject $object -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $object -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $object -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + # Select all of the columns you'd like to show + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, LogDate, ProcessInfo, Text + } + } + catch { + Stop-Function -Continue -Target $server -Message "Could not read from SQL Server Agent" + } + } + } + else { + try { + foreach ($object in $server.JobServer.ReadErrorLog()) { + Write-Message -Level Verbose -Message "Processing $object" + Add-Member -Force -InputObject $object -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $object -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $object -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + # Select all of the columns you'd like to show + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, LogDate, ProcessInfo, Text + } + } + catch { + Stop-Function -Continue -Target $server -Message "Could not read from SQL Server Agent" + } + } + } + } +} +function Get-DbaAgentOperator { + <# + .SYNOPSIS + Returns all SQL Agent operators on a SQL Server Agent. + + .DESCRIPTION + This function returns SQL Agent operators. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Operator + The operator(s) to process - this list is auto-populated from the server. If unspecified, all operators will be processed. + + .PARAMETER ExcludeOperator + The operator(s) to exclude - this list is auto-populated from the server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Operator + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentOperator + + .EXAMPLE + Get-DbaAgentOperator -SqlInstance ServerA,ServerB\instanceB + + Returns any SQL Agent operators on serverA and serverB\instanceB + + .EXAMPLE + 'ServerA','ServerB\instanceB' | Get-DbaAgentOperator + + Returns all SQL Agent operators on serverA and serverB\instanceB + + .EXAMPLE + Get-DbaAgentOperator -SqlInstance ServerA -Operator Dba1,Dba2 + + Returns only the SQL Agent Operators Dba1 and Dba2 on ServerA. + + .EXAMPLE + Get-DbaAgentOperator -SqlInstance ServerA,ServerB -ExcludeOperator Dba3 + + Returns all the SQL Agent operators on ServerA and ServerB, except the Dba3 operator. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [object[]]$Operator, + [object[]]$ExcludeOperator, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Verbose -Message "Getting Edition from $server" + Write-Message -Level Verbose -Message "$server is a $($server.Edition)" + + if ($server.Edition -like 'Express*') { + Stop-Function -Message "There is no SQL Agent on $server, it's a $($server.Edition)" -Continue -Target $server + } + + $defaults = "ComputerName", "SqlInstance", "InstanceName", "Name", "ID", "Enabled as IsEnabled", "EmailAddress", "LastEmail" + + if ($Operator) { + $operators = $server.JobServer.Operators | Where-Object Name -In $Operator + } + elseif ($ExcludeOperator) { + $operators = $server.JobServer.Operators | Where-Object Name -NotIn $ExcludeOperator + } + else { + $operators = $server.JobServer.Operators + } + + foreach ($operat in $operators) { + + $jobs = $server.JobServer.jobs | Where-Object { $_.OperatorToEmail, $_.OperatorToNetSend, $_.OperatorToPage -contains $operat.Name } + $lastemail = [dbadatetime]$operat.LastEmailDate + + Add-Member -Force -InputObject $operat -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $operat -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $operat -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $operat -MemberType NoteProperty -Name RelatedJobs -Value $jobs + Add-Member -Force -InputObject $operat -MemberType NoteProperty -Name LastEmail -Value $lastemail + Select-DefaultView -InputObject $operat -Property $defaults + } + } + } +} +function Get-DbaAgentSchedule { + <# + .SYNOPSIS + Returns all SQL Agent Shared Schedules on a SQL Server Agent. + + .DESCRIPTION + This function returns SQL Agent Shared Schedules. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Schedule + Parameter to filter the schedules returned + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Schedule + Author: Chris McKeown (@devopsfu), http://www.devopsfu.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgentSchedule + + .EXAMPLE + Get-DbaAgentSchedule -SqlInstance localhost + + Returns all SQL Agent Shared Schedules on the local default SQL Server instance + + .EXAMPLE + Get-DbaAgentSchedule -SqlInstance localhost, sql2016 + + Returns all SQL Agent Shared Schedules for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "Instance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Schedules")] + [object[]]$Schedule, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Get-ScheduleDescription { + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$Schedule + + ) + + # Get the culture to make sure the right date and time format is displayed + $datetimeFormat = (Get-culture).DateTimeFormat + + # Set the intial description + $description = "" + + # Get the date and time values + $startDate = Get-Date $Schedule.ActiveStartDate -format $datetimeFormat.ShortDatePattern + $startTime = Get-Date ($Schedule.ActiveStartTimeOfDay.ToString()) -format $datetimeFormat.LongTimePattern + $endDate = Get-Date $Schedule.ActiveEndDate -format $datetimeFormat.ShortDatePattern + $endTime = Get-Date ($Schedule.ActiveEndTimeOfDay.ToString()) -format $datetimeFormat.LongTimePattern + + # Start setting the description based on the frequency type + switch ($schedule.FrequencyTypes) { + {($_ -eq 1) -or ($_ -eq "Once")} { $description += "Occurs on $startDate at $startTime" } + {($_ -in 4, 8, 16, 32) -or ($_ -in "Daily", "Weekly", "Monthly")} { $description += "Occurs every "} + {($_ -eq 64) -or ($_ -eq "AutoStart")} {$description += "Start automatically when SQL Server Agent starts "} + {($_ -eq 128) -or ($_ -eq "OnIdle")} {$description += "Start whenever the CPUs become idle"} + } + + # Check the frequency types for daily or weekly i.e. + switch ($schedule.FrequencyTypes) { + # Daily + {$_ -in 4, "Daily"} { + if ($Schedule.FrequencyInterval -eq 1) { + $description += "day " + } + elseif ($Schedule.FrequencyInterval -gt 1) { + $description += "$($Schedule.FrequencyInterval) day(s) " + } + } + + # Weekly + {$_ -in 8, "Weekly"} { + # Check if it's for one or more weeks + if ($Schedule.FrequencyRecurrenceFactor -eq 1) { + $description += "week on " + } + elseif ($Schedule.FrequencyRecurrenceFactor -gt 1) { + $description += "$($Schedule.FrequencyRecurrenceFactor) week(s) on " + } + + # Save the interval for the loop + $frequencyInterval = $Schedule.FrequencyInterval + + # Create the array to hold the days + $days = ($false, $false, $false, $false, $false, $false, $false) + + # Loop through the days + while ($frequencyInterval -gt 0) { + + switch ($FrequenctInterval) { + {($frequencyInterval - 64) -ge 0} { + $days[5] = "Saturday" + $frequencyInterval -= 64 + } + {($frequencyInterval - 32) -ge 0} { + $days[4] = "Friday" + $frequencyInterval -= 32 + } + {($frequencyInterval - 16) -ge 0} { + $days[3] = "Thursday" + $frequencyInterval -= 16 + } + {($frequencyInterval - 8) -ge 0} { + $days[2] = "Wednesday" + $frequencyInterval -= 8 + } + {($frequencyInterval - 4) -ge 0} { + $days[1] = "Tuesday" + $frequencyInterval -= 4 + } + {($frequencyInterval - 2) -ge 0} { + $days[0] = "Monday" + $frequencyInterval -= 2 + } + {($frequencyInterval - 1) -ge 0} { + $days[6] = "Sunday" + $frequencyInterval -= 1 + } + } + + } + + # Add the days to the description by selecting the days and exploding the array + $description += ($days | Where-Object {$_ -ne $false}) -join ", " + $description += " " + + } + + # Monthly + {$_ -in 16, "Monthly"} { + # Check if it's for one or more months + if ($Schedule.FrequencyRecurrenceFactor -eq 1) { + $description += "month " + } + elseif ($Schedule.FrequencyRecurrenceFactor -gt 1) { + $description += "$($Schedule.FrequencyRecurrenceFactor) month(s) " + } + + # Add the interval + $description += "on day $($Schedule.FrequencyInterval) of that month " + } + + # Monthly relative + {$_ -in 32, "MonthlyRelative"} { + # Check for the relative day + switch ($Schedule.FrequencyRelativeIntervals) { + {$_ -in 1, "First"} {$description += "first "} + {$_ -in 2, "Second"} {$description += "second "} + {$_ -in 4, "Third"} {$description += "third "} + {$_ -in 8, "Fourth"} {$description += "fourth "} + {$_ -in 16, "Last"} {$description += "last "} + } + + # Get the relative day of the week + switch ($Schedule.FrequencyInterval) { + 1 { $description += "Sunday "} + 2 { $description += "Monday "} + 3 { $description += "Tuesday "} + 4 { $description += "Wednesday "} + 5 { $description += "Thursday "} + 6 { $description += "Friday "} + 7 { $description += "Saturday "} + 8 { $description += "Day "} + 9 { $description += "Weekday "} + 10 { $description += "Weekend day "} + } + + $description += "of every $($Schedule.FrequencyRecurrenceFactor) month(s) " + + } + } + + # Check the frequency type + if ($schedule.FrequencyTypes -notin 64, 128) { + + # Check the subday types for minutes or hours i.e. + if ($schedule.FrequencySubDayInterval -in 0, 1) { + $description += "at $startTime. " + } + else { + + switch ($Schedule.FrequencySubDayTypes) { + {$_ -in 2, "Seconds"} { $description += "every $($schedule.FrequencySubDayInterval) second(s) "} + {$_ -in 4, "Minutes"} {$description += "every $($schedule.FrequencySubDayInterval) minute(s) " } + {$_ -in 8, "Hours"} { $description += "every $($schedule.FrequencySubDayInterval) hour(s) " } + } + + $description += "between $startTime and $endTime. " + } + + # Check if an end date has been given + if ($Schedule.ActiveEndDate.Year -eq 9999) { + $description += "Schedule will be used starting on $startDate." + } + else { + $description += "Schedule will used between $startDate and $endDate." + } + } + + return $description + } + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.Edition -like 'Express*') { + Stop-Function -Message "$($server.Edition) does not support SQL Server Agent. Skipping $server." -Continue + } + + if ($Schedule) { + $scheduleCollection = $server.JobServer.SharedSchedules | Where-Object { $_.Name -in $Schedule } + } + else { + $scheduleCollection = $server.JobServer.SharedSchedules + } + + } + + $defaults = "ComputerName", "InstanceName", "SqlInstance", "Name as ScheduleName", "ActiveEndDate", "ActiveEndTimeOfDay", "ActiveStartDate", "ActiveStartTimeOfDay", "DateCreated", "FrequencyInterval", "FrequencyRecurrenceFactor", "FrequencyRelativeIntervals", "FrequencySubDayInterval", "FrequencySubDayTypes", "FrequencyTypes", "IsEnabled", "JobCount", "Description" + + foreach ($schedule in $scheduleCollection) { + $description = Get-ScheduleDescription -Schedule $schedule + + Add-Member -Force -InputObject $schedule -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $schedule -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $schedule -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $schedule -MemberType NoteProperty Description -Value $description + + Select-DefaultView -InputObject $schedule -Property $defaults + } + + } +} +function Get-DbaAgHadr { + <# + .SYNOPSIS + Gets the Hadr service setting on the specified SQL Server instance. + + .DESCRIPTION + Gets the Hadr setting, from the service level, and returns true or false for the specified SQL Server instance. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL instance + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgHadr + + .EXAMPLE + Get-DbaAgHadr -SqlInstance sql2016 + + Returns a status of the Hadr setting for sql2016 SQL Server instance. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $server -Property 'ComputerName', 'InstanceName', 'SqlInstance', 'IsHadrEnabled' + } + } +} +function Get-DbaAgListener { + <# + .SYNOPSIS + Outputs the name of the Listener for the Availability Group(s) found on the server. + + .DESCRIPTION + Default view provides most common set of properties for information on the database in an Availability Group(s). + + Information returned on the database will be specific to that replica, whether it is primary or a secondary. + + This command will return an SMO object, but it is the AvailabilityDatabases object and not the Server.Databases object. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2012 or higher. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). + + .PARAMETER AvailabilityGroup + Specify the Availability Group name that you want to get information on. + + .PARAMETER Listener + Specify the Listener name that you want to get information on. + + .PARAMETER InputObject + Piped in Availability Group objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: AG, AvailabilityGroup, Listener + Author: Viorel Ciucu (@viorelciucu) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgListener + + .EXAMPLE + Get-DbaAgListener -SqlInstance sqlserver2014a + + Returns basic information on the listener found on sqlserver2014a + + .EXAMPLE + Get-DbaAgListener -SqlInstance sqlserver2014a -AvailabilityGroup AG-a + + Returns basic information on the listener found on sqlserver2014a in the Availability Group AG-a + + #> + [CmdletBinding()] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ValueFromPipeline = $true)] + [string[]]$AvailabilityGroup, + [string[]]$Listener, + [object[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaAvailabilityGroup -SqlInstance $instance -SqlCredential $SqlCredential -AvailabilityGroup $AvailabilityGroup + } + if (Test-Bound -ParameterName Listener) { + $InputObject = $InputObject | Where-Object { $_.AvailabilityGroupListeners.Name -contains $Listener } + } + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'AvailabilityGroup', 'Name', 'PortNumber', 'ClusterIPConfiguration' + foreach ($aglistener in $InputObject.AvailabilityGroupListeners) { + $server = $aglistener.Parent.Parent + Add-Member -Force -InputObject $aglistener -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $aglistener -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $aglistener -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $aglistener -MemberType NoteProperty -Name AvailabilityGroup -value $aglistener.Parent.Name + Select-DefaultView -InputObject $aglistener -Property $defaults + } + } +} +function Get-DbaAgReplica { + <# + .SYNOPSIS + Outputs the Availability Group(s)' Replica object found on the server. + + .DESCRIPTION + Default view provides most common set of properties for information on the Availability Group(s)' Replica. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2012 or higher. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER AvailabilityGroup + Specify the Availability Group name that you want to get information on. + + .PARAMETER Replica + Specify the replica to pull information on, is dependent up name that you want to get information on. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: AG, AvailabilityGroup, Replica + Author: Shawn Melton (@wsmelton) | Chrissy LeMaire (@ctrlb) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAgReplica + + .EXAMPLE + Get-DbaAgReplica -SqlInstance sqlserver2014a + + Returns basic information on all the Availability Group(s) replica(s) found on sqlserver2014a + + .EXAMPLE + Get-DbaAgReplica -SqlInstance sqlserver2014a -AvailabilityGroup AG-a + + Shows basic information on the replica(s) found on Availability Group AG-a on sqlserver2014a + + .EXAMPLE + Get-DbaAgReplica -SqlInstance sqlserver2014a | Select * + + Returns full object properties on all Availability Group(s) replica(s) on sqlserver2014a + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ValueFromPipeline = $true)] + [object[]]$AvailabilityGroup, + [object[]]$Replica, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($serverName in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $serverName -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.IsHadrEnabled -eq $false) { + Stop-Function -Message "Availability Group (HADR) is not configured for the instance: $serverName" -Target $serverName -Continue + } + + $ags = $server.AvailabilityGroups + if ($AvailabilityGroup) { + $ags = $ags | Where-Object Name -in $AvailabilityGroup + } + + foreach ($ag in $ags) { + $replicas = $ag.AvailabilityReplicas + foreach ($currentReplica in $replicas) { + if ($Replica -and $currentReplica.Name -notmatch $Replica) { + continue + } + + Add-Member -Force -InputObject $currentReplica -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $currentReplica -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $currentReplica -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Parent as AvailabilityGroup', 'Name as Replica', 'Role', 'ConnectionState', 'RollupSynchronizationState', 'AvailabilityMode', 'BackupPriority', 'EndpointUrl', 'SessionTimeout', 'FailoverMode', 'ReadonlyRoutingList' + Select-DefaultView -InputObject $currentReplica -Property $defaults + } + } + } + } +} +function Get-DbaAvailabilityGroup { + <# + .SYNOPSIS + Outputs the Availability Group(s) object found on the server. + + .DESCRIPTION + Default view provides most common set of properties for information on the Availability Group(s). + + .PARAMETER SqlInstance + The SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2012 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER AvailabilityGroup + Specifies the Availability Group name that you want to get information on. + + .PARAMETER IsPrimary + If this switch is enabled, a boolean indicating whether SqlInstance is the Primary replica in the AG is returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Hadr, AG, AvailabilityGroup + Author: Shawn Melton (@wsmelton) | Chrissy LeMaire (@ctrlb) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAvailabilityGroup + + .EXAMPLE + Get-DbaAvailabilityGroup -SqlInstance sqlserver2014a + + Returns basic information on all the Availability Group(s) found on sqlserver2014a. + + .EXAMPLE + Get-DbaAvailabilityGroup -SqlInstance sqlserver2014a -AvailabilityGroup AG-a + + Shows basic information on the Availability Group AG-a on sqlserver2014a. + + .EXAMPLE + Get-DbaAvailabilityGroup -SqlInstance sqlserver2014a | Select * + + Returns full object properties on all Availability Group(s) on sqlserver2014a. + + .EXAMPLE + Get-DbaAvailabilityGroup -SqlInstance sqlserver2014a -AvailabilityGroup AG-a -IsPrimary + + Returns true/false if the server, sqlserver2014a, is the primary replica for AG-a Availability Group. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$AvailabilityGroup, + [switch]$IsPrimary, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($serverName in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $serverName -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.IsHadrEnabled -eq $false) { + Stop-Function -Message "Availability Group (HADR) is not configured for the instance: $serverName." -Target $serverName -Continue + } + + $ags = $server.AvailabilityGroups + if ($AvailabilityGroup) { + $ags = $ags | Where-Object Name -in $AvailabilityGroup + } + + foreach ($ag in $ags) { + Add-Member -Force -InputObject $ag -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $ag -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $ag -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + if ($IsPrimary) { + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name as AvailabilityGroup', 'IsPrimary' + $value = $false + if ($ag.PrimaryReplicaServerName -eq $server.Name) { + $value = $true + } + Add-Member -Force -InputObject $ag -MemberType NoteProperty -Name IsPrimary -Value $value + Select-DefaultView -InputObject $ag -Property $defaults + } + else { + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'LocalReplicaRole', 'Name as AvailabilityGroup', 'PrimaryReplicaServerName as PrimaryReplica', 'AutomatedBackupPreference', 'AvailabilityReplicas', 'AvailabilityDatabases', 'AvailabilityGroupListeners' + Select-DefaultView -InputObject $ag -Property $defaults + } + } + } + } +} +function Get-DbaAvailableCollation { + <# + .SYNOPSIS + Function to get available collations for a given SQL Server + + .DESCRIPTION + The Get-DbaAvailableCollation function returns the list of collations available on each SQL Server. + Only the connect permission is required to get this information. + + .PARAMETER SqlInstance + The SQL Server instance, or instances. Only connect permission is required. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Collation, Configuration + Author: Bryan Hamby (@galador) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaAvailableCollation + + .EXAMPLE + Get-DbaAvailableCollation -SqlInstance sql2016 + + Gets all the collations from server sql2016 using NT authentication + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + #Functions to get/cache the code page and language description. + #It runs about 9x faster caching these (2 vs 18 seconds) in my test, + #since there are so many duplicates + + #No longer supported by Windows, but still shows up in SQL Server + #http://www.databaseteam.org/1-ms-sql-server/982faddda7a789a1.htm + $locales = @{66577 = "Japanese_Unicode"} + $codePages = @{} + + function Get-LocaleDescription ($LocaleId) { + if ($locales.ContainsKey($LocaleId)) { + $localeName = $locales.Get_Item($LocaleId) + } + else { + try { + $localeName = (Get-Language $LocaleId).DisplayName + } + catch { + $localeName = $null + } + $locales.Set_Item($LocaleId, $localeName) + } + return $localeName + } + + function Get-CodePageDescription ($codePageId) { + if ($codePages.ContainsKey($codePageId)) { + $codePageName = $codePages.Get_Item($codePageId) + } + else { + try { + $codePageName = (Get-CodePage $codePageId).EncodingName + } + catch { + $codePageName = $null + } + $codePages.Set_Item($codePageId, $codePageName) + } + return $codePageName + } + } + + process { + foreach ($Instance in $sqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $availableCollations = $server.EnumCollations() + foreach ($collation in $availableCollations) { + Add-Member -Force -InputObject $collation -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $collation -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $collation -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $collation -MemberType NoteProperty -Name CodePageName -Value (Get-CodePageDescription $collation.CodePage) + Add-Member -Force -InputObject $collation -MemberType NoteProperty -Name LocaleName -Value (Get-LocaleDescription $collation.LocaleID) + } + + Select-DefaultView -InputObject $availableCollations -Property ComputerName, InstanceName, SqlInstance, Name, CodePage, CodePageName, LocaleID, LocaleName, Description + } + } +} +#ValidationTags#Messaging# +function Get-DbaBackupDevice { + <# + .SYNOPSIS + Gets SQL Backup Device information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaBackupDevice command gets SQL Backup Device information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaBackupDevice + + .EXAMPLE + Get-DbaBackupDevice -SqlInstance localhost + + Returns all Backup Devices on the local default SQL Server instance + + .EXAMPLE + Get-DbaBackupDevice -SqlInstance localhost, sql2016 + + Returns all Backup Devices for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($backupDevice in $server.BackupDevices) { + Add-Member -Force -InputObject $backupDevice -MemberType NoteProperty -Name ComputerName -value $backupDevice.Parent.NetName + Add-Member -Force -InputObject $backupDevice -MemberType NoteProperty -Name InstanceName -value $backupDevice.Parent.ServiceName + Add-Member -Force -InputObject $backupDevice -MemberType NoteProperty -Name SqlInstance -value $backupDevice.Parent.DomainInstanceName + + Select-DefaultView -InputObject $backupDevice -Property ComputerName, InstanceName, SqlInstance, Name, BackupDeviceType, PhysicalLocation, SkipTapeLabel + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaBackupHistory { + <# + .SYNOPSIS + Returns backup history details for databases on a SQL Server. + + .DESCRIPTION + Returns backup history details for some or all databases on a SQL Server. + + You can even get detailed information (including file path) for latest full, differential and log files. + + Backups taken with the CopyOnly option will NOT be returned, unless the IncludeCopyOnly switch is present + + Reference: http://www.sqlhub.com/2011/07/find-your-backup-history-in-sql-server.html + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server Instance as a different user. This can be a Windows or SQL Server account. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER IncludeCopyOnly + By default Get-DbaBackupHistory will ignore backups taken with the CopyOnly option. This switch will include them + + .PARAMETER Force + If this switch is enabled, a large amount of information is returned, similar to what SQL Server itself returns. + + .PARAMETER Since + Specifies a DateTime object to use as the starting point for the search for backups. + + .PARAMETER Last + If this switch is enabled, the most recent full chain of full, diff and log backup sets is returned. + + .PARAMETER LastFull + If this switch is enabled, the most recent full backup set is returned. + + .PARAMETER LastDiff + If this switch is enabled, the most recent differential backup set is returned. + + .PARAMETER LastLog + If this switch is enabled, the most recent log backup is returned. + + .PARAMETER DeviceType + Specifies a filter for backup sets based on DeviceTypes. Valid options are 'Disk','Permanent Disk Device', 'Tape', 'Permanent Tape Device','Pipe','Permanent Pipe Device','Virtual Device', in addition to custom integers for your own DeviceTypes. + + .PARAMETER Raw + If this switch is enabled, one object per backup file is returned. Otherwise, media sets (striped backups across multiple files) will be grouped into a single return object. + + .PARAMETER Type + Specifies one or more types of backups to return. Valid options are 'Full', 'Log', 'Differential', 'File', 'Differential File', 'Partial Full', and 'Partial Differential'. Otherwise, all types of backups will be returned unless one of the -Last* switches is enabled. + + .PARAMETER LastLsn + Specifies a minimum LSN to use in filtering backup history. Only backups with an LSN greater than this value will be returned, which helps speed the retrieval process. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaBackupHistory + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance SqlInstance2014a + + Returns server name, database, username, backup type, date for all backups databases on SqlInstance2014a. This may return many rows; consider using filters that are included in other examples. + + .EXAMPLE + $cred = Get-Credential sqladmin + Get-DbaBackupHistory -SqlInstance SqlInstance2014a -SqlCredential $cred + + Does the same as above but logs in as SQL user "sqladmin" + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance SqlInstance2014a -Database db1, db2 -Since '7/1/2016 10:47:00' + + Returns backup information only for databases db1 and db2 on SqlInstance2014a since July 1, 2016 at 10:47 AM. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014, pubs -Force | Format-Table + + Returns information only for AdventureWorks2014 and pubs and formats the results as a table. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014 -Last + + Returns information about the most recent full, differential and log backups for AdventureWorks2014 on sql2014. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014 -Last -DeviceType Disk + + Returns information about the most recent full, differential and log backups for AdventureWorks2014 on sql2014, but only for backups to disk. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014 -Last -DeviceType 148,107 + + Returns information about the most recent full, differential and log backups for AdventureWorks2014 on sql2014, but only for backups with device_type 148 and 107. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014 -LastFull + + Returns information about the most recent full backup for AdventureWorks2014 on sql2014. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance sql2014 -Database AdventureWorks2014 -Type Full + + Returns information about all Full backups for AdventureWorks2014 on sql2014. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2016 | Get-DbaBackupHistory + + Returns database backup information for every database on every server listed in the Central Management Server on sql2016. + + .EXAMPLE + Get-DbaBackupHistory -SqlInstance SqlInstance2014a, sql2016 -Force + + Returns detailed backup history for all databases on SqlInstance2014a and sql2016. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance, + [Alias("Credential")] + [PsCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeCopyOnly, + [Parameter(ParameterSetName = "NoLast")] + [switch]$Force, + [Parameter(ParameterSetName = "NoLast")] + [DateTime]$Since, + [Parameter(ParameterSetName = "Last")] + [switch]$Last, + [Parameter(ParameterSetName = "Last")] + [switch]$LastFull, + [Parameter(ParameterSetName = "Last")] + [switch]$LastDiff, + [Parameter(ParameterSetName = "Last")] + [switch]$LastLog, + [string[]]$DeviceType, + [switch]$Raw, + [bigint]$LastLsn, + [ValidateSet("Full", "Log", "Differential", "File", "Differential File", "Partial Full", "Partial Differential")] + [string[]]$Type, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Write-Message -Level System -Message "Active Parameter set: $($PSCmdlet.ParameterSetName)." + Write-Message -Level System -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $deviceTypeMapping = @{ + 'Disk' = 2 + 'Permanent Disk Device' = 102 + 'Tape' = 5 + 'Permanent Tape Device' = 105 + 'Pipe' = 6 + 'Permanent Pipe Device' = 106 + 'Virtual Device' = 7 + 'URL' = 9 + } + $deviceTypeFilter = @() + foreach ($devType in $DeviceType) { + if ($devType -in $deviceTypeMapping.Keys) { + $deviceTypeFilter += $deviceTypeMapping[$devType] + } + else { + $deviceTypeFilter += $devType + } + } + $backupTypeMapping = @{ + 'Log' = 'L' + 'Full' = 'D' + 'File' = 'F' + 'Differential' = 'I' + 'Differential File' = 'G' + 'Partial Full' = 'P' + 'Partial Differential' = 'Q' + } + $backupTypeFilter = @() + foreach ($typeFilter in $Type) { + $backupTypeFilter += $backupTypeMapping[$typeFilter] + } + + } + + process { + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance." -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -ge 10) { + $compressedFlag = $true + # 2008 introduced compressed_backup_size + $backupCols = " + backupset.backup_size AS TotalSize, + backupset.compressed_backup_size as CompressedBackupSize" + } + else { + $compressedFlag = $false + $backupCols = " + backupset.backup_size AS TotalSize, + NULL as CompressedBackupSize" + } + + $databases = @() + if ($null -ne $Database) { + foreach ($db in $Database) { + $databases += [PSCustomObject]@{name = $db} + } + } + else { + $databases = $server.Databases + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + foreach ($d in $deviceTypeFilter) { + $deviceTypeFilterRight = "IN ('" + ($deviceTypeFilter -Join "','") + "')" + } + + foreach ($b in $backupTypeFilter) { + $backupTypeFilterRight = "IN ('" + ($backupTypeFilter -Join "','") + "')" + } + + if ($last) { + foreach ($db in $databases) { + + #Get the full and build upwards + $allBackups = @() + $allBackups += $fullDb = Get-DbaBackupHistory -SqlInstance $server -Database $db.Name -LastFull -raw:$Raw -DeviceType $DeviceType -IncludeCopyOnly:$IncludeCopyOnly + $diffDb = Get-DbaBackupHistory -SqlInstance $server -Database $db.Name -LastDiff -raw:$Raw -DeviceType $DeviceType -IncludeCopyOnly:$IncludeCopyOnly + if ($diffDb.LastLsn -gt $fullDb.LastLsn -and $diffDb.DatabaseBackupLSN -eq $fullDb.CheckPointLSN ) { + Write-Message -Level Verbose -Message "Valid Differential backup " + $allBackups += $diffDb + $tlogStartDsn = ($diffDb.FirstLsn -as [bigint]) + } + else { + Write-Message -Level Verbose -Message "No Diff found" + try { + [bigint]$tlogStartDsn = $fullDb.FirstLsn.ToString() + } + catch { + continue + } + } + $allBackups += Get-DbaBackupHistory -SqlInstance $server -Database $db.Name -raw:$raw -DeviceType $DeviceType -LastLsn $tlogStartDsn -IncludeCopyOnly:$IncludeCopyOnly | Where-Object { + $_.Type -eq 'Log' -and [bigint]$_.LastLsn -gt [bigint]$tlogStartDsn -and [bigint]$_.DatabaseBackupLSN -eq [bigint]$fullDb.CheckPointLSN -and $_.LastRecoveryForkGuid -eq $fullDb.LastRecoveryForkGuid + } + #This line does the output for -Last!!! + $allBackups | Sort-Object -Property LastLsn, Type + } + continue + } + + if ($LastFull -or $LastDiff -or $LastLog) { + if ($LastFull) { + $first = 'D'; $second = 'P' + } + if ($LastDiff) { + $first = 'I'; $second = 'Q' + } + if ($LastLog) { + $first = 'L'; $second = 'L' + } + $databases = $databases | Select-Object -Unique -Property Name + $sql = "" + foreach ($db in $databases) { + Write-Message -Level Verbose -Message "Processing $($db.name)" -Target $db + $whereCopyOnly = $null + if ($true -ne $IncludeCopyOnly) { + $whereCopyOnly = " AND is_copy_only='0' " + } + if ($deviceTypeFilter) { + $devTypeFilterWhere = "AND mediafamily.device_type $deviceTypeFilterRight" + } + # recap for future editors (as this has been discussed over and over): + # - original editors (from hereon referred as "we") rank over backupset.last_lsn desc, backupset.backup_finish_date desc for a good reason: DST + # all times are recorded with the timezone of the server + # - we thought about ranking over backupset.backup_set_id desc, backupset.last_lsn desc, backupset.backup_finish_date desc + # but there is no explicit documentation about "when" a row gets inserted into backupset. Theoretically it _could_ + # happen that backup_set_id for the same database has not the same order of last_lsn. + # - given ultimately to restore something lsn IS the source of truth, we decided to trust that and only that + # - we know that sometimes it happens to drop a database without deleting the history. Assuming then to create a database with the same name, + # and given the lsn are composed in the first part by the VLF SeqID, it happens seldomly that for the same database_name backupset holds + # last_lsn out of order. To avoid this behaviour, we filter by database_guid choosing the guid that has MAX(backup_finish_date), as we know + # last_lsn cannot be out-of-order for the same database, and the same database cannot have different database_guid + $sql += " + SELECT + a.BackupSetRank, + a.Server, + a.[Database], + a.Username, + a.Start, + a.[End], + a.Duration, + a.[Path], + a.Type, + a.TotalSize, + a.CompressedBackupSize, + a.MediaSetId, + a.BackupSetID, + a.Software, + a.position, + a.first_lsn, + a.database_backup_lsn, + a.checkpoint_lsn, + a.last_lsn, + a.first_lsn as 'FirstLSN', + a.database_backup_lsn as 'DatabaseBackupLsn', + a.checkpoint_lsn as 'CheckpointLsn', + a.last_lsn as 'LastLsn', + a.software_major_version, + a.DeviceType, + a.is_copy_only, + a.last_recovery_fork_guid + FROM (SELECT + RANK() OVER (ORDER BY backupset.last_lsn desc, backupset.backup_finish_date DESC) AS 'BackupSetRank', + backupset.database_name AS [Database], + backupset.user_name AS Username, + backupset.backup_start_date AS Start, + backupset.server_name as [Server], + backupset.backup_finish_date AS [End], + DATEDIFF(SECOND, backupset.backup_start_date, backupset.backup_finish_date) AS Duration, + mediafamily.physical_device_name AS Path, + $backupCols, + CASE backupset.type + WHEN 'L' THEN 'Log' + WHEN 'D' THEN 'Full' + WHEN 'F' THEN 'File' + WHEN 'I' THEN 'Differential' + WHEN 'G' THEN 'Differential File' + WHEN 'P' THEN 'Partial Full' + WHEN 'Q' THEN 'Partial Differential' + ELSE NULL + END AS Type, + backupset.media_set_id AS MediaSetId, + mediafamily.media_family_id as mediafamilyid, + backupset.backup_set_id as BackupSetID, + CASE mediafamily.device_type + WHEN 2 THEN 'Disk' + WHEN 102 THEN 'Permanent Disk Device' + WHEN 5 THEN 'Tape' + WHEN 105 THEN 'Permanent Tape Device' + WHEN 6 THEN 'Pipe' + WHEN 106 THEN 'Permanent Pipe Device' + WHEN 7 THEN 'Virtual Device' + WHEN 9 THEN 'URL' + ELSE 'Unknown' + END AS DeviceType, + backupset.position, + backupset.first_lsn, + backupset.database_backup_lsn, + backupset.checkpoint_lsn, + backupset.last_lsn, + backupset.software_major_version, + mediaset.software_name AS Software, + backupset.is_copy_only, + backupset.last_recovery_fork_guid, + backupset.recovery_model + FROM msdb..backupmediafamily AS mediafamily + JOIN msdb..backupmediaset AS mediaset + ON mediafamily.media_set_id = mediaset.media_set_id + JOIN msdb..backupset AS backupset + ON backupset.media_set_id = mediaset.media_set_id + JOIN ( + SELECT DISTINCT database_guid, database_name, backup_finish_date + FROM msdb..backupset + WHERE backupset.database_name = '$($db.Name)' + ) dbguid + ON dbguid.database_name = backupset.database_name + AND dbguid.database_guid = backupset.database_guid + JOIN ( + SELECT database_name, MAX(backup_finish_date) max_finish_date + FROM msdb..backupset + WHERE backupset.database_name = '$($db.Name)' + GROUP BY database_name + ) dbguid_support + ON dbguid_support.database_name = backupset.database_name + AND dbguid.backup_finish_date = dbguid_support.max_finish_date + WHERE backupset.database_name = '$($db.Name)' $whereCopyOnly + AND (type = '$first' OR type = '$second') + $devTypeFilterWhere + ) AS a + WHERE a.BackupSetRank = 1 + ORDER BY a.Type; + " + } + $sql = $sql -join "; " + } + else { + if ($Force -eq $true) { + $select = "SELECT * " + } + else { + $select = " + SELECT + backupset.database_name AS [Database], + backupset.user_name AS Username, + backupset.server_name as [server], + backupset.backup_start_date AS [Start], + backupset.backup_finish_date AS [End], + DATEDIFF(SECOND, backupset.backup_start_date, backupset.backup_finish_date) AS Duration, + mediafamily.physical_device_name AS Path, + $backupCols, + CASE backupset.type + WHEN 'L' THEN 'Log' + WHEN 'D' THEN 'Full' + WHEN 'F' THEN 'File' + WHEN 'I' THEN 'Differential' + WHEN 'G' THEN 'Differential File' + WHEN 'P' THEN 'Partial Full' + WHEN 'Q' THEN 'Partial Differential' + ELSE NULL + END AS Type, + backupset.media_set_id AS MediaSetId, + mediafamily.media_family_id as MediaFamilyId, + backupset.backup_set_id as BackupSetId, + CASE mediafamily.device_type + WHEN 2 THEN 'Disk' + WHEN 102 THEN 'Permanent Disk Device' + WHEN 5 THEN 'Tape' + WHEN 105 THEN 'Permanent Tape Device' + WHEN 6 THEN 'Pipe' + WHEN 106 THEN 'Permanent Pipe Device' + WHEN 7 THEN 'Virtual Device' + WHEN 9 THEN 'URL' + ELSE 'Unknown' + END AS DeviceType, + backupset.position, + backupset.first_lsn, + backupset.database_backup_lsn, + backupset.checkpoint_lsn, + backupset.last_lsn, + backupset.first_lsn as 'FirstLSN', + backupset.database_backup_lsn as 'DatabaseBackupLsn', + backupset.checkpoint_lsn as 'CheckpointLsn', + backupset.last_lsn as 'LastLsn', + backupset.software_major_version, + mediaset.software_name AS Software, + backupset.is_copy_only, + backupset.last_recovery_fork_guid, + backupset.recovery_model" + } + + $from = " FROM msdb..backupmediafamily mediafamily + INNER JOIN msdb..backupmediaset mediaset ON mediafamily.media_set_id = mediaset.media_set_id + INNER JOIN msdb..backupset backupset ON backupset.media_set_id = mediaset.media_set_id" + if ($Database -or $Since -or $Last -or $LastFull -or $LastLog -or $LastDiff -or $deviceTypeFilter -or $LastLsn -or $backupTypeFilter) { + $where = " WHERE " + } + + $whereArray = @() + + if ($Database.length -gt 0) { + $dbList = $Database -join "','" + $whereArray += "database_name IN ('$dbList')" + } + + if ($true -ne $IncludeCopyOnly) { + $whereArray += "is_copy_only='0'" + } + + if ($Last -or $LastFull -or $LastLog -or $LastDiff) { + $tempWhere = $whereArray -join " AND " + $whereArray += "type = 'Full' AND mediaset.media_set_id = (SELECT TOP 1 mediaset.media_set_id $from $tempWhere ORDER BY backupset.last_lsn DESC)" + } + + if ($null -ne $Since) { + $whereArray += "backupset.backup_finish_date >= '$($Since.ToString("yyyy-MM-ddTHH:mm:ss"))'" + } + + if ($deviceTypeFilter) { + $whereArray += "mediafamily.device_type $deviceTypeFilterRight" + } + if ($backupTypeFilter) { + $whereArray += "backupset.type $backupTypeFilterRight" + } + + if ($LastLsn) { + $whereArray += "backupset.last_lsn > $LastLsn" + } + if ($where.Length -gt 0) { + $whereArray = $whereArray -join " AND " + $where = "$where $whereArray" + } + + $sql = "$select $from $where ORDER BY backupset.last_lsn DESC" + } + + Write-Message -Level Debug -Message "SQL Statement: `n$sql" + Write-Message -Level SomewhatVerbose -Message "Executing sql query." + $results = $server.ConnectionContext.ExecuteWithResults($sql).Tables.Rows | Select-Object * -ExcludeProperty BackupSetRank, RowError, RowState, Table, ItemArray, HasErrors + + if ($raw) { + Write-Message -Level SomewhatVerbose -Message "Processing as Raw Output." + $results | Select-Object *, @{ Name = "FullName"; Expression = { $_.Path } } + Write-Message -Level SomewhatVerbose -Message "$($results.Count) result sets found." + } + else { + Write-Message -Level SomewhatVerbose -Message "Processing as grouped output." + $groupedResults = $results | Group-Object -Property BackupsetId + Write-Message -Level SomewhatVerbose -Message "$($groupedResults.Count) result-groups found." + $groupResults = @() + $backupSetIds = $groupedResults.Name + $backupSetIdsList = $backupSetIds -Join "," + if ($groupedResults.Count -gt 0) { + $backupSetIdsWhere = "backup_set_id IN ($backupSetIdsList)" + $fileAllSql = "SELECT backup_set_id, file_type as FileType, logical_name as LogicalName, physical_name as PhysicalName + FROM msdb..backupfile WHERE $backupSetIdsWhere" + Write-Message -Level Debug -Message "FileSQL: $fileAllSql" + $fileListResults = $server.Query($fileAllSql) + } + else { + $fileListResults = @() + } + $fileListHash = @{} + foreach ($fl in $fileListResults) { + if (-not($fileListHash.ContainsKey($fl.backup_set_id))) { + $fileListHash[$fl.backup_set_id] = @() + } + $fileListHash[$fl.backup_set_id] += $fl + } + foreach ($group in $groupedResults) { + $commonFields = $group.Group[0] + $groupLength = $group.Group.Count + if ($groupLength -eq 1) { + $start = $commonFields.Start + $end = $commonFields.End + $duration = New-TimeSpan -Seconds $commonFields.Duration + } + else { + $start = ($group.Group.Start | Measure-Object -Minimum).Minimum + $end = ($group.Group.End | Measure-Object -Maximum).Maximum + $duration = New-TimeSpan -Seconds ($group.Group.Duration | Measure-Object -Maximum).Maximum + } + $compressedBackupSize = $commonFields.CompressedBackupSize + if ($compressedFlag -eq $true) { + $ratio = [Math]::Round(($commonFields.TotalSize) / ($compressedBackupSize), 2) + } + else { + $compressedBackupSize = $null + $ratio = 1 + } + $historyObject = New-Object Sqlcollaborative.Dbatools.Database.BackupHistory + $historyObject.ComputerName = $server.NetName + $historyObject.InstanceName = $server.ServiceName + $historyObject.SqlInstance = $server.DomainInstanceName + $historyObject.Database = $commonFields.Database + $historyObject.UserName = $commonFields.UserName + $historyObject.Start = $start + $historyObject.End = $end + $historyObject.Duration = $duration + $historyObject.Path = $group.Group.Path + $historyObject.TotalSize = $commonFields.TotalSize + $historyObject.CompressedBackupSize = $compressedBackupSize + $historyObject.CompressionRatio = $ratio + $historyObject.Type = $commonFields.Type + $historyObject.BackupSetId = $commonFields.BackupSetId + $historyObject.DeviceType = $commonFields.DeviceType + $historyObject.Software = $commonFields.Software + $historyObject.FullName = $group.Group.Path + $historyObject.FileList = $fileListHash[$commonFields.BackupSetID] | Select-Object FileType, LogicalName, PhysicalName + $historyObject.Position = $commonFields.Position + $historyObject.FirstLsn = $commonFields.First_LSN + $historyObject.DatabaseBackupLsn = $commonFields.database_backup_lsn + $historyObject.CheckpointLsn = $commonFields.checkpoint_lsn + $historyObject.LastLsn = $commonFields.Last_Lsn + $historyObject.SoftwareVersionMajor = $commonFields.Software_Major_Version + $historyObject.IsCopyOnly = ($commonFields.is_copy_only -eq 1) + $historyObject.LastRecoveryForkGuid = $commonFields.last_recovery_fork_guid + $historyObject.RecoveryModel = $commonFields.Recovery_Model + $historyObject + } + $groupResults | Sort-Object -Property LastLsn, Type + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaBackupInformation { + <# + .SYNOPSIS + Scan backup files and creates a set, compatible with Restore-DbaDatabase + + .DESCRIPTION + Upon being passed a list of potential backups files this command will scan the files, select those that contain SQL Server + backup sets. It will then filter those files down to a set + + The function defaults to working on a remote instance. This means that all paths passed in must be relative to the remote instance. + XpDirTree will be used to perform the file scans + + Various means can be used to pass in a list of files to be considered. The default is to non recursively scan the folder + passed in. + + .PARAMETER Path + Path to SQL Server backup files. + + Paths passed in as strings will be scanned using the desired method, default is a non recursive folder scan + Accepts multiple paths separated by ',' + + Or it can consist of FileInfo objects, such as the output of Get-ChildItem or Get-Item. This allows you to work with + your own file structures as needed + + .PARAMETER SqlInstance + The SQL Server instance to be used to read the headers of the backup files + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER DatabaseName + An array of Database Names to filter by. If empty all databases are returned. + + .PARAMETER SourceInstance + If provided only backup originating from this destination will be returned. This SQL instance will not be connected to or involved in this work + + .PARAMETER NoXpDirTree + If this switch is set, then Files will be parsed as locally files. This can cause failures if the running user can see files that the parsing SQL Instance cannot + + .PARAMETER DirectoryRecurse + If specified the provided path/directory will be traversed (only applies if not using XpDirTree) + + .PARAMETER Anonymise + If specified we will output the results with ComputerName, InstanceName, Database, UserName, and Paths hashed out + This options is mainly for use if we need you to submit details for fault finding to the dbatools team + + .PARAMETER ExportPath + If specified the output will export via CliXml format to the specified file. This allows you to store the backup history object for later usage, or move it between computers + + .PARAMETER NoClobber + If specified will stop Export from overwriting an existing file, the default is to overwrite + + .PARAMETER PassThru + When data is exported the cmdlet will return no other output, this switch means it will also return the normal output which can be then piped into another command + + .PARAMETER MaintenanceSolution + This switch tells the function that the folder is the root of a Ola Hallengren backup folder + + .PARAMETER IgnoreLogBackup + This switch only works with the MaintenanceSolution switch. With an Ola Hallengren style backup we can be sure that the LOG folder contains only log backups and skip it. + For all other scenarios we need to read the file headers to be sure. + + .PARAMETER AzureCredential + The name of the SQL Server credential to be used if restoring from an Azure hosted backup + + .PARAMETER Import + When specified along with a path the command will import a previously exported BackupHistory object from an xml file. + + .PARAMETER EnableException + Replaces user friendly yellow warnings with bloody red exceptions of doom! + Use this if you want the function to throw terminating errors you want to catch. + + .EXAMPLE + Get-DbaBackupInformation -SqlInstance Server1 -Path c:\backups\ -DirectoryRecurse + + Will use the Server1 instance to recursively read all backup files under c:\backups, and return a dbatools BackupHistory object + + .NOTES + Tags: DisasterRecovery, Backup, Restore + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaBackupInformation + + .EXAMPLE + Get-DbaBackupInformation -SqlInstance Server1 -Path c:\backups\ -DirectoryRecurse -ExportPath c:\store\BackupHistory.xml + + #Copy the file c:\store\BackupHistory.xml to another machine via preferred technique, and the on 2nd machine: + + Get-DbaBackupInformation -Import -Path c:\store\BackupHistory.xml | Restore-DbaDatabase -SqlInstance Server2 -TrustDbBackupHistory + + This allows you to move backup history across servers, or to preserve backup history even after the original server has been purged + + .EXAMPLE + Get-DbaBackupInformation -SqlInstance Server1 -Path c:\backups\ -DirectoryRecurse -ExportPath c:\store\BackupHistory.xml -PassThru | + Restore-DbaDatabase -SqlInstance Server2 -TrustDbBackupHistory + + In this example we gather backup information, export it to an xml file, and then pass it on through to Restore-DbaDatabase + This allows us to repeat the restore without having to scan all the backup files again + + .EXAMPLE + Get-ChildItem c:\backups\ -recurse -files | + Where {$_.extension -in ('.bak','.trn') -and $_.LastWriteTime -gt (get-date).AddMonths(-1)} | + Get-DbaBackupInformation -SqlInstance Server1 -ExportPath c:\backupHistory.xml + + This lets you keep a record of all backup history from the last month on hand to speed up refreshes + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\network\backups + $Backups += Get-DbaBackupInformation -SqlInstance Server2 -NoXpDirTree -Path c:\backups + + Scan the unc folder \\network\backups with Server1, and then scan the C:\backups folder on + Server2 not using xp_dirtree, adding the results to the first set. + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\network\backups -MaintenanceSolution + + When MaintenanceSolution is indicated we know we are dealing with the output from Ola Hallengren's backup scripts. So we make sure that a FULL folder exists in the first level of Path, if not we shortcut scanning all the files as we have nothing to work with + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\network\backups -MaintenanceSolution -IgnoreLogBackup + + As we know we are dealing with an Ola Hallengren style backup folder from the MaintenanceSolution switch, when IgnoreLogBackup is also included we can ignore the LOG folder to skip any scanning of log backups. Note this also means then WON'T be restored + #> + [CmdletBinding( DefaultParameterSetName = "Create")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Path, + [parameter(Mandatory = $true, ParameterSetName = "Create")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(ParameterSetName = "Create")] + [PSCredential]$SqlCredential, + [string[]]$DatabaseName, + [string[]]$SourceInstance, + [parameter(ParameterSetName = "Create")] + [Switch]$NoXpDirTree, + [parameter(ParameterSetName = "Create")] + [switch]$DirectoryRecurse, + [switch]$EnableException, + [switch]$MaintenanceSolution, + [switch]$IgnoreLogBackup, + [string]$ExportPath, + [string]$AzureCredential, + [parameter(ParameterSetName = "Import")] + [switch]$Import, + [switch][Alias('Anonymize')]$Anonymise, + [Switch]$NoClobber, + [Switch]$PassThru + + ) + begin { + function Get-HashString { + param( + [String]$InString + ) + + $StringBuilder = New-Object System.Text.StringBuilder + [System.Security.Cryptography.HashAlgorithm]::Create("md5").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($InString))| ForEach-Object { + [Void]$StringBuilder.Append($_.ToString("x2")) + } + return $StringBuilder.ToString() + } + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Debug -Message "Parameters bound: $($PSBoundParameters.Keys -join ", ")" + + if (Test-Bound -ParameterName ExportPath) { + if ($true -eq $NoClobber) { + if (Test-Path $ExportPath) { + Stop-Function -Message "$ExportPath exists and NoClobber set" + return + } + } + } + if ($PSCmdlet.ParameterSetName -eq "Create") { + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + } + + if ($true -eq $IgnoreLogBackup -and $true -ne $MaintenanceSolution) { + Write-Message -Message "IgnoreLogBackup can only by used with MaintenanceSolution. Will not be used" -Level Warning + } + } + process { + if (Test-FunctionInterrupt) { return } + if ((Test-Bound -Parameter Import) -and ($true -eq $Import)) { + foreach ($f in $Path) { + if (Test-Path -Path $f) { + $GroupResults += Import-CliXml -Path $f + foreach ($group in $GroupResults) { + $Group.FirstLsn = [BigInt]$group.FirstLSN.ToString() + $Group.CheckpointLSN = [BigInt]$group.CheckpointLSN.ToString() + $Group.DatabaseBackupLsn = [BigInt]$group.DatabaseBackupLsn.ToString() + $Group.LastLsn = [BigInt]$group.LastLsn.ToString() + } + } + else { + Write-Message -Message "$f does not exist or is unreadable" -Level Warning + } + } + } + else { + $Files = @() + $groupResults = @() + if ($Path[0] -match 'http') { $NoXpDirTree = $true } + if ($NoXpDirTree -ne $true) { + foreach ($f in $path) { + if ([System.IO.Path]::GetExtension($f).Length -gt 1) { + if ("Fullname" -notin $f.PSobject.Properties.name) { + $f = $f | Select-Object *, @{ Name = "FullName"; Expression = { $f } } + } + Write-Message -Message "Testing a single file $f " -Level Verbose + if ((Test-DbaSqlPath -Path $f.fullname -SqlInstance $server)) { + $files += $f + } + else { + Write-Message -Level Verbose -Message "$server cannot 'see' file $($f.FullName)" + } + } + elseif ($True -eq $MaintenanceSolution) { + if ($true -eq $IgnoreLogBackup -and [System.IO.Path]::GetDirectoryName($f) -like '*LOG') { + Write-Message -Level Verbose -Message "Skipping Log Backups as requested" + } + else { + Write-Message -Level Verbose -Message "OLA - Getting folder contents" + $Files += Get-XpDirTreeRestoreFile -Path $f -SqlInstance $server + } + } + else { + Write-Message -Message "Testing a folder $f" -Level Verbose + $Files += $Check = Get-XpDirTreeRestoreFile -Path $f -SqlInstance $server + if ($null -eq $check) { + Write-Message -Message "Nothing returned from $f" -Level Verbose + } + } + } + } + else { + ForEach ($f in $path) { + Write-Message -Level VeryVerbose -Message "Not using sql for $f" + if ($f -is [System.IO.FileSystemInfo]) { + if ($f.PsIsContainer -eq $true -and $true -ne $MaintenanceSolution) { + Write-Message -Level VeryVerbose -Message "folder $($f.fullname)" + $Files += Get-ChildItem -Path $f.fullname -File -Recurse:$DirectoryRecurse + } + elseif ($f.PsIsContainer -eq $true -and $true -eq $MaintenanceSolution) { + if ($IgnoreLogBackup -and $f -notlike '*LOG' ) { + Write-Message -Level Verbose -Message "Skipping Log backups for Maintenance backups" + } + else { + $Files += Get-ChildItem -Path $f.fullname -File -Recurse:$DirectoryRecurse + } + } + elseif ($true -eq $MaintenanceSolution) { + $Files += Get-ChildItem -Path $f.fullname -Recurse:$DirectoryRecurse + } + else { + Write-Message -Level VeryVerbose -Message "File" + $Files += $f.fullname + } + } + else { + if ($true -eq $MaintenanceSolution) { + $Files += Get-XpDirTreeRestoreFile -Path $f\FULL -SqlInstance $server -NoRecurse + $Files += Get-XpDirTreeRestoreFile -Path $f\DIFF -SqlInstance $server -NoRecurse + $Files += Get-XpDirTreeRestoreFile -Path $f\LOG -SqlInstance $server -NoRecurse + } + else { + Write-Message -Level VeryVerbose -Message "File" + $Files += $f + } + } + } + } + + if ($True -eq $MaintenanceSolution -and $True -eq $IgnoreLogBackup) { + Write-Message -Level Verbose -Message "Skipping Log Backups as requested" + $Files = $Files | Where-Object {$_.FullName -notlike '*\LOG\*'} + } + + Write-Message -Level Verbose -Message "Reading backup headers of $($Files.Count) files" + $FileDetails = Read-DbaBackupHeader -SqlInstance $server -Path $Files -AzureCredential $AzureCredential + + $groupdetails = $FileDetails | group-object -Property BackupSetGUID + + foreach ($Group in $GroupDetails) { + $historyObject = New-Object Sqlcollaborative.Dbatools.Database.BackupHistory + $historyObject.ComputerName = $group.group[0].MachineName + $historyObject.InstanceName = $group.group[0].ServiceName + $historyObject.SqlInstance = $group.group[0].ServerName + $historyObject.Database = $group.Group[0].DatabaseName + $historyObject.UserName = $group.Group[0].UserName + $historyObject.Start = [DateTime]$group.Group[0].BackupStartDate + $historyObject.End = [DateTime]$group.Group[0].BackupFinishDate + $historyObject.Duration = ([DateTime]$group.Group[0].BackupFinishDate - [DateTime]$group.Group[0].BackupStartDate) + $historyObject.Path = [string[]]$Group.Group.BackupPath + $historyObject.FileList = ($group.Group.FileList | select-object Type, LogicalName, PhysicalName) + $historyObject.TotalSize = ($Group.Group.BackupSize | Measure-Object -Sum).Sum + $HistoryObject.CompressedBackupSize = ($Group.Group.CompressedBackupSize | Measure-Object -Sum).Sum + $historyObject.Type = $group.Group[0].BackupTypeDescription + $historyObject.BackupSetId = $group.group[0].BackupSetGUID + $historyObject.DeviceType = 'Disk' + $historyObject.FullName = $Group.Group.BackupPath + $historyObject.Position = $group.Group[0].Position + $historyObject.FirstLsn = $group.Group[0].FirstLSN + $historyObject.DatabaseBackupLsn = $group.Group[0].DatabaseBackupLSN + $historyObject.CheckpointLSN = $group.Group[0].CheckpointLSN + $historyObject.LastLsn = $group.Group[0].LastLsn + $historyObject.SoftwareVersionMajor = $group.Group[0].SoftwareVersionMajor + $historyObject.RecoveryModel = $group.Group.RecoveryModel + $groupResults += $historyObject + } + } + if (Test-Bound 'SourceInstance') { + $groupResults = $groupResults | Where-Object {$_.InstanceName -in $SourceInstance} + } + + if (Test-Bound 'DatabaseName') { + $groupResults = $groupResults | Where-Object {$_.Database -in $DatabaseName} + } + if ($true -eq $Anonymise) { + foreach ($group in $GroupResults) { + $group.ComputerName = Get-HashString -InString $group.ComputerName + $group.InstanceName = Get-HashString -InString $group.InstanceName + $group.SqlInstance = Get-HashString -InString $group.SqlInstance + $group.Database = Get-HashString -InString $group.Database + $group.UserName = Get-HashString -InString $group.UserName + $group.Path = Get-HashString -InString $Group.Path + $group.FullName = Get-HashString -InString $Group.Fullname + } + } + if ((Test-Bound -parameterName exportpath) -and $null -ne $ExportPath) { + $groupResults | Export-CliXml -Path $ExportPath -Depth 5 -NoClobber:$NoClobber + if ($true -ne $PassThru) { + return + } + } + $groupResults | Sort-Object -Property End -Descending + } +} +function Get-DbaClientAlias { + <# + .SYNOPSIS + Creates/updates a sql alias for the specified server - mimics cliconfg.exe + + .DESCRIPTION + Creates/updates a SQL Server alias by altering HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client + + .PARAMETER ComputerName + The target computer where the alias will be created + + .PARAMETER Credential + Allows you to login to remote computers using alternative credentials + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Alias + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaClientAlias + + .EXAMPLE + Get-DbaClientAlias + Gets all SQL Server client aliases on the local computer + + .EXAMPLE + Get-DbaClientAlias -ComputerName workstationx + Gets all SQL Server client aliases on Workstationx +#> + [CmdletBinding()] + Param ( + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($computer in $ComputerName) { + $scriptblock = { + + function Get-ItemPropertyValue { + Param ( + [parameter()] + [String]$Path, + [parameter()] + [String]$Name + ) + (Get-ItemProperty -LiteralPath $Path -Name $Name).$Name + } + + $basekeys = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer", "HKLM:\SOFTWARE\Microsoft\MSSQLServer" + + foreach ($basekey in $basekeys) { + + if ((Test-Path $basekey) -eq $false) { + Write-Warning "Base key ($basekey) does not exist. Quitting." + continue + } + + $client = "$basekey\Client" + + if ((Test-Path $client) -eq $false) { + continue + } + + $connect = "$client\ConnectTo" + + if ((Test-Path $connect) -eq $false) { + continue + } + + if ($basekey -like "*WOW64*") { + $architecture = "32-bit" + } + else { + $architecture = "64-bit" + } + + # "Creating/updating alias for $ComputerName for $architecture" + $all = Get-Item -Path $connect + foreach ($entry in $all.Property) { + $value = Get-ItemPropertyValue -Path $connect -Name $entry + $clean = $value.Replace('DBNMPNTW,', '').Replace('DBMSSOCN,', '') + if ($value.StartsWith('DBMSSOCN')) { $protocol = 'TCP/IP' } else { $protocol = 'Named Pipes' } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + NetworkLibrary = $protocol + ServerName = $clean + AliasName = $entry + AliasString = $value + Architecture = $architecture + } + } + } + } + + if ($PScmdlet.ShouldProcess($computer, "Getting aliases")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -Property ComputerName, Architecture, NetworkLibrary, ServerName, AliasName + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} +function Get-DbaClientProtocol { + <# + .SYNOPSIS + Gets the SQL Server related client protocols on a computer. + + .DESCRIPTION + Gets the SQL Server related client protocols on one or more computers. + + Requires Local Admin rights on destination computer(s). + The client protocols can be enabled and disabled when retrieved via WSMan. + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Protocol + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaClientProtocol + + .EXAMPLE + Get-DbaClientProtocol -ComputerName sqlserver2014a + + Gets the SQL Server related client protocols on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaClientProtocol + + Gets the SQL Server related client protocols on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaClientProtocol -ComputerName sql1,sql2 | Out-Gridview + + Gets the SQL Server related client protocols on computers sql1 and sql2, and shows them in a grid view. + + .EXAMPLE + (Get-DbaClientProtocol -ComputerName sql2 | Where { $_.DisplayName = 'via' }).Disable() + + Disables the VIA ClientNetworkProtocol on computer sql2. + If succesfull, returncode 0 is shown. +#> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ( $computer in $ComputerName.ComputerName ) { + $server = Resolve-DbaNetworkName -ComputerName $computer -Credential $credential + if ( $server.FullComputerName ) { + $computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Getting SQL Server namespace on $computer" -EnableException $EnableException + $namespace = Get-DbaCmObject -ComputerName $computer -Namespace root\Microsoft\SQLServer -Query "Select * FROM __NAMESPACE WHERE Name LIke 'ComputerManagement%'" -ErrorAction SilentlyContinue | + Where-Object {(Get-DbaCmObject -ComputerName $computer -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -ClassName ClientNetworkProtocol -ErrorAction SilentlyContinue).count -gt 0} | + Sort-Object Name -Descending | Select-Object -First 1 + + if ( $namespace.Name ) { + Write-Message -Level Verbose -Message "Getting Cim class ClientNetworkProtocol in Namespace $($namespace.Name) on $computer" -EnableException $EnableException + try { + $prot = Get-DbaCmObject -ComputerName $computer -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -ClassName ClientNetworkProtocol -ErrorAction SilentlyContinue + + $prot | Add-Member -Force -MemberType ScriptProperty -Name IsEnabled -Value { switch ( $this.ProtocolOrder ) { 0 { $false } default { $true } } } + $prot | Add-Member -Force -MemberType ScriptMethod -Name Enable -Value {Invoke-CimMethod -MethodName SetEnable -InputObject $this } + $prot | Add-Member -Force -MemberType ScriptMethod -Name Disable -Value {Invoke-CimMethod -MethodName SetDisable -InputObject $this } + + foreach ( $protocol in $prot ) { + Select-DefaultView -InputObject $protocol -Property 'PSComputerName as ComputerName', 'ProtocolDisplayName as DisplayName', 'ProtocolDll as DLL', 'ProtocolOrder as Order', 'IsEnabled' + } + } + catch { + Write-Message -Level Warning -Message "No Sql ClientNetworkProtocol found on $computer" -EnableException $EnableException + } + } #if namespace + else { + Write-Message -Level Warning -Message "No ComputerManagement Namespace on $computer. Please note that this function is available from SQL 2005 up." -EnableException $EnableException + } #else no namespace + } #if computername + else { + Write-Message -Level Warning -Message "Failed to connect to $computer" + } + } #foreach computer + } +} +function Get-DbaClusterNode { + <# + .SYNOPSIS + Returns the node(s) of a SQL Cluster. + + .DESCRIPTION + Returns the name of the current node(s) in the SQL Server cluster. + + If the -ActiveNode Parameter is passed it only returns the name of the Server currently hosting the clustered instance. + + .PARAMETER SqlInstance + Specifies the SQL Server clustered instance to check. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ActiveNode + If this parameter is selected the cmdlet will only return the Active Node in the cluster. + + .PARAMETER Detailed + Output all properties, will be deprecated in 1.0.0 release. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Cluster, WSFC, FCI + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaClusterNode + + .EXAMPLE + Get-DbaClusterNode -SqlInstance sqlcluster + + Returns all nodes in the cluster and details about each node. + + .EXAMPLE + Get-DbaClusterNode -SqlInstance sqlcluster -ActiveNode + + Returns the name of the active node in the cluster + + #> + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$ActiveNode, + [switch]$Detailed, + [alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Parameter Detailed + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Alias Get-DbaClusterActiveNode + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -MinimumVersion 10 + } + + process { + if ($server.IsClustered -eq $false) { + Stop-Function -Message "Not a clusterd instance." -Category ConnectionError -ErrorRecord $_ -Target $SqlInstance -Continue + } + + # If the -ActiveNode switch is selected only the primary node is returned. + if ($ActiveNode) { + try { + $sql = "SELECT * FROM sys.dm_os_cluster_nodes where is_current_owner = 1" + $datatable = $server.query($sql) + + [PSCustomObject]@{ + ComputerName = $datatable.nodename + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Status = $datatable.Status + StatusDescription = $datatable.StatusDescription + CurrentOwner = $datatable.is_current_owner + } | Select-DefaultView -Property ComputerName + } + catch { + Stop-Function -Message "Unable to query sys.dm_os_cluster_nodes on $server." -ErrorRecord $_ -Target $SqlInstance -Continue + } + } + #Default Execution of this function + else { + try { + $sql = "SELECT * FROM sys.dm_os_cluster_nodes" + $datatable = $server.query($sql) + + foreach ($data in $datatable) { + [PSCustomObject]@{ + ComputerName = $data.nodename + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Status = $data.Status + StatusDescription = $data.StatusDescription + CurrentOwner = $data.is_current_owner + } | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, StatusDescription, CurrentOwner + } + } + catch { + Stop-Function -Message "Unable to query sys.dm_os_cluster_nodes on $server." -ErrorRecord $_ -Target $SqlInstance -Continue + } + } + } +} +function Get-DbaCmConnection { + <# + .SYNOPSIS + Retrieves windows management connections from the cache + + .DESCRIPTION + Retrieves windows management connections from the cache + + .PARAMETER ComputerName + The computername to ComputerName for. + + .PARAMETER UserName + Username on credentials to look for. Will not find connections using the default windows credentials. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaCmConnection + + .EXAMPLE + Get-DbaCmConnection + + List all cached connections. + + .EXAMPLE + Get-DbaCmConnection sql2014 + + List the cached connection - if any - to the server sql2014. + + .EXAMPLE + Get-DbaCmConnection -UserName "*charles*" + + List all cached connection that use a username containing "charles" as default or override credentials. + #> + [CmdletBinding()] + param + ( + [Parameter(Position = 0, ValueFromPipeline = $true)] + [Alias('Filter')] + [String[]] + $ComputerName = "*", + + [String] + $UserName = "*", + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + } + PROCESS { + foreach ($name in $ComputerName) { + Write-Message -Level VeryVerbose -Message "Processing search. ComputerName: '$name' | Username: '$UserName'" + ([Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections.Values | Where-Object { ($_.ComputerName -like $name) -and ($_.Credentials.UserName -like $UserName) }) + } + } + END { + Write-Message -Level InternalComment -Message "Ending" + } +} +function Get-DbaCmObject { + <# + .SYNOPSIS + Retrieves Wmi/Cim-Style information from computers. + + .DESCRIPTION + This function centralizes all requests for information retrieved from Get-WmiObject or Get-CimInstance. + It uses different protocols as available in this order: + - Cim over WinRM + - Cim over DCOM + - Wmi + - Wmi over PowerShell Remoting + It remembers channels that didn't work and will henceforth avoid them. It remembers invalid credentials and will avoid reusing them. + Much of its behavior can be configured using Test-DbaWmConnection. + + .PARAMETER ClassName + The name of the class to retrieve. + + .PARAMETER Query + The Wmi/Cim query tu run against the server. + + .PARAMETER ComputerName + The computer(s) to connect to. Defaults to localhost. + + .PARAMETER Credential + Credentials to use. Invalid credentials will be stored in a credentials cache and not be reused. + + .PARAMETER Namespace + The namespace of the class to use. + + .PARAMETER DoNotUse + Connection Protocols that should not be used. + + .PARAMETER Force + Overrides some checks that might otherwise halt execution as a precaution + - Ignores timeout on bad connections + + .PARAMETER SilentlyContinue + Use in conjunction with the -EnableException switch. + By default, Get-DbaCmObject will throw a terminating exception when connecting to a target is impossible in exception enabled mode. + Setting this switch will cause it write a non-terminating exception and continue with the next computer. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaCmObject + + .EXAMPLE + Get-DbaCmObject win32_OperatingSystem + + Retrieves the common operating system information from the local computer. + + .EXAMPLE + Get-DbaCmObject -Computername "sql2014" -ClassName Win32_OperatingSystem -Credential $cred -DoNotUse CimRM + + Retrieves the common operating system information from the server sql2014. + It will use the credewntials stored in $cred to connect, unless they are known to not work, in which case they will default to windows credentials (unless another default has been set). + #> + [CmdletBinding(DefaultParameterSetName = "Class")] + param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Class")] + [Alias('Class')] + [string] + $ClassName, + + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Query")] + [string] + $Query, + + [Parameter(ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName = $env:COMPUTERNAME, + + [System.Management.Automation.PSCredential] + $Credential, + + [string] + $Namespace = "root\cimv2", + + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]] + $DoNotUse = "None", + + [switch] + $Force, + + [switch] + $SilentlyContinue, + + [switch] + [Alias('Silent')]$EnableException + ) + + Begin { + #region Configuration Values + $disable_cache = [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::DisableCache + + Write-Message -Level Verbose -Message "Configuration loaded | Cache disabled: $disable_cache" + #endregion Configuration Values + + $ParSet = $PSCmdlet.ParameterSetName + } + Process { + :main foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret input: $($connectionObject.Input)" -Category InvalidArgument -Target $connectionObject.Input -Continue -SilentlyContinue:$SilentlyContinue } + + # Since all connection caching runs using lower-case strings, making it lowercase here simplifies things. + $computer = $connectionObject.Connection.ComputerName.ToLower() + + Write-Message -Message "[$computer] Retrieving Management Information" -Level VeryVerbose -Target $computer + + $connection = $connectionObject.Connection + + # Ensure using the right credentials + try { $cred = $connection.GetCredential($Credential) } + catch { + $message = "Bad credentials! " + if ($Credential) { $message += "The credentials for $($Credential.UserName) are known to not work. " } + else { $message += "The windows credentials are known to not work. " } + if ($connection.EnableCredentialFailover -or $connection.OverrideExplicitCredential) { $message += "The connection is configured to use credentials that are known to be good, but none have been registered yet. " } + elseif ($connection.Credentials) { $message += "Working credentials are known for $($connection.Credentials.UserName), however the connection is not configured to automatically use them. This can be done using 'Set-DbaCmConnection -ComputerName $connection -OverrideExplicitCredential' " } + elseif ($connection.UseWindowsCredentials) { $message += "The windows credentials are known to work, however the connection is not configured to automatically use them. This can be done using 'Set-DbaCmConnection -ComputerName $connection -OverrideExplicitCredential' " } + $message += $_.Exception.Message + Stop-Function -Message $message -ErrorRecord $_ -Target $connection -Continue -OverrideExceptionMessage + } + + # Flags-Enumerations cannot be added in PowerShell 4 or older. + # Thus we create a string and convert it afterwards. + $enabledProtocols = "None" + if ($connection.CimRM -notlike "Disabled") { $enabledProtocols += ", CimRM" } + if ($connection.CimDCOM -notlike "Disabled") { $enabledProtocols += ", CimDCOM" } + if ($connection.Wmi -notlike "Disabled") { $enabledProtocols += ", Wmi" } + if ($connection.PowerShellRemoting -notlike "Disabled") { $enabledProtocols += ", PowerShellRemoting" } + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType]$enabledProtocols = $enabledProtocols + + # Create list of excluded connection types (Duplicates don't matter) + $excluded = @() + foreach ($item in $DoNotUse) { $excluded += $item } + + :sub while ($true) { + try { $conType = $connection.GetConnectionType(($excluded -join ","), $Force) } + catch { + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + Stop-Function -Message "[$computer] Unable to find a connection to the target system. Ensure the name is typed correctly, and the server allows any of the following protocols: $enabledProtocols" -Target $computer -Category OpenError -Continue -ContinueLabel "main" -SilentlyContinue:$SilentlyContinue -ErrorRecord $_ + } + + switch ($conType.ToString()) { + #region CimRM + "CimRM" { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over WinRM" + try { + if ($ParSet -eq "Class") { $connection.GetCimRMInstance($cred, $ClassName, $Namespace) } + else { $connection.QueryCimRMInstance($cred, $Query, "WQL", $Namespace) } + + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over WinRM - Success!" + $connection.ReportSuccess('CimRM') + $connection.AddGoodCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + continue main + } + catch { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over WinRM - Failed!" + + # 1 = Generic runtime error + if ($_.Exception.InnerException.StatusCode -eq 1) { + # 0x8007052e, 0x80070005 : Authentication error, bad credential + if (($_.Exception.InnerException -eq 0x8007052e) -or ($_.Exception.InnerException -eq 0x80070005)) { + # Ignore the global setting for bad credential cache disabling, since the connection object is aware of that state and will ignore input if it should. + # This is due to the ability to locally override the global setting, thus it must be done on the object and can then be done in code + $connection.AddBadCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + Stop-Function -Message "[$computer] Invalid connection credentials" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + elseif ($_.Exception.InnerException.MessageId -eq "HRESULT 0x80041013") { + if ($ParSet -eq "Class") { Stop-Function -Message "[$computer] Failed to access $class in namespace $Namespace!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -Exception $_.Exception.InnerException } + else { Stop-Function -Message "[$computer] Failed to execute $query in namespace $Namespace!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -Exception $_.Exception.InnerException } + } + else { + $connection.ReportFailure('CimRM') + $excluded += "CimRM" + continue sub + } + } + + # 2 = Access to specific resource denied + elseif ($_.Exception.InnerException.StatusCode -eq 2) { + Stop-Function -Message "[$computer] Access to computer granted, but access to $Namespace\$ClassName denied!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + + # 3 = Invalid Namespace + elseif ($_.Exception.InnerException.StatusCode -eq 3) { + Stop-Function -Message "[$computer] Invalid namespace: $Namespace" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + # 5 = Invalid Class + # See here for code reference: https://msdn.microsoft.com/en-us/library/cc150671(v=vs.85).aspx + elseif ($_.Exception.InnerException.StatusCode -eq 5) { + Stop-Function -Message "[$computer] Invalid class name ($ClassName), not found in current namespace ($Namespace)" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + + # 0 & ExtendedStatus = Weird issue beyond the scope of the CIM standard. Often a server-side issue + elseif (($_.Exception.InnerException.StatusCode -eq 0) -and ($_.Exception.InnerException.ErrorData.original_error -like "__ExtendedStatus")) { + Stop-Function -Message "[$computer] Something went wrong when looking for $ClassName, in $Namespace. This often indicates issues with the target system." -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue + } + else { + $connection.ReportFailure('CimRM') + $excluded += "CimRM" + continue sub + } + } + } + #endregion CimRM + + #region CimDCOM + "CimDCOM" { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over DCOM" + try { + if ($ParSet -eq "Class") { $connection.GetCimDCOMInstance($cred, $ClassName, $Namespace) } + else { $connection.QueryCimDCOMInstance($cred, $Query, "WQL", $Namespace) } + + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over DCOM - Success!" + $connection.ReportSuccess('CimDCOM') + $connection.AddGoodCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + continue main + } + catch { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using Cim over DCOM - Failed!" + + # 1 = Generic runtime error + if ($_.Exception.InnerException.StatusCode -eq 1) { + # 0x8007052e, 0x80070005 : Authentication error, bad credential + if (($_.Exception.InnerException -eq 0x8007052e) -or ($_.Exception.InnerException -eq 0x80070005)) { + # Ignore the global setting for bad credential cache disabling, since the connection object is aware of that state and will ignore input if it should. + # This is due to the ability to locally override the global setting, thus it must be done on the object and can then be done in code + $connection.AddBadCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + Stop-Function -Message "[$computer] Invalid connection credentials" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + elseif ($_.Exception.InnerException.MessageId -eq "HRESULT 0x80041013") { + if ($ParSet -eq "Class") { Stop-Function -Message "[$computer] Failed to access $class in namespace $Namespace!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -Exception $_.Exception.InnerException } + else { Stop-Function -Message "[$computer] Failed to execute $query in namespace $Namespace!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -Exception $_.Exception.InnerException } + } + else { + $connection.ReportFailure('CimDCOM') + $excluded += "CimDCOM" + continue sub + } + } + + # 2 = Access to specific resource denied + elseif ($_.Exception.InnerException.StatusCode -eq 2) { + Stop-Function -Message "[$computer] Access to computer granted, but access to $Namespace\$ClassName denied!" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + + # 3 = Invalid Namespace + elseif ($_.Exception.InnerException.StatusCode -eq 3) { + Stop-Function -Message "[$computer] Invalid namespace: $Namespace" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + + # 5 = Invalid Class + # See here for code reference: https://msdn.microsoft.com/en-us/library/cc150671(v=vs.85).aspx + elseif ($_.Exception.InnerException.StatusCode -eq 5) { + Stop-Function -Message "[$computer] Invalid class name ($ClassName), not found in current namespace ($Namespace)" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue -OverrideExceptionMessage + } + + # 0 & ExtendedStatus = Weird issue beyond the scope of the CIM standard. Often a server-side issue + elseif (($_.Exception.InnerException.StatusCode -eq 0) -and ($_.Exception.InnerException.ErrorData.original_error -like "__ExtendedStatus")) { + Stop-Function -Message "[$computer] Something went wrong when looking for $ClassName, in $Namespace. This often indicates issues with the target system." -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue + } + + else { + $connection.ReportFailure('CimDCOM') + $excluded += "CimDCOM" + continue sub + } + } + } + #endregion CimDCOM + + #region Wmi + "Wmi" { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using WMI" + try { + switch ($ParSet) { + "Class" { + $parameters = @{ + ComputerName = $computer + ClassName = $ClassName + ErrorAction = 'Stop' + } + if ($cred) { $parameters["Credential"] = $cred } + if (Test-Bound "Namespace") { $parameters["Namespace"] = $Namespace } + + } + "Query" { + $parameters = @{ + ComputerName = $computer + Query = $Query + ErrorAction = 'Stop' + } + if ($cred) { $parameters["Credential"] = $cred } + if (Test-Bound "Namespace") { $parameters["Namespace"] = $Namespace } + } + } + + Get-WmiObject @parameters + + Write-Message -Level Verbose -Message "[$computer] Accessing computer using WMI - Success!" + $connection.ReportSuccess('Wmi') + $connection.AddGoodCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + continue main + } + catch { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using WMI - Failed!" -ErrorRecord $_ + + if ($_.CategoryInfo.Reason -eq "UnauthorizedAccessException") { + # Ignore the global setting for bad credential cache disabling, since the connection object is aware of that state and will ignore input if it should. + # This is due to the ability to locally override the global setting, thus it must be done on the object and can then be done in code + $connection.AddBadCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + Stop-Function -Message "[$computer] Invalid connection credentials" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue + } + elseif ($_.CategoryInfo.Category -eq "InvalidType") { + Stop-Function -Message "[$computer] Invalid class name ($ClassName), not found in current namespace ($Namespace)" -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue + } + elseif ($_.Exception.ErrorCode -eq "ProviderLoadFailure") { + Stop-Function -Message "[$computer] Failed to access: $ClassName, in namespace: $Namespace - There was a provider error. This indicates a potential issue with WMI on the server side." -Target $computer -Continue -ContinueLabel "main" -ErrorRecord $_ -SilentlyContinue:$SilentlyContinue + } + else { + $connection.ReportFailure('Wmi') + $excluded += "Wmi" + continue sub + } + } + } + #endregion Wmi + + #region PowerShell Remoting + "PowerShellRemoting" { + try { + Write-Message -Level Verbose -Message "[$computer] Accessing computer using PowerShell Remoting" + $scp_string = "Get-WmiObject -Class $ClassName -ErrorAction Stop" + if ($PSBoundParameters.ContainsKey("Namespace")) { $scp_string += " -Namespace $Namespace" } + + $parameters = @{ + ScriptBlock = ([System.Management.Automation.ScriptBlock]::Create($scp_string)) + ComputerName = $ComputerName + ErrorAction = 'Stop' + } + if ($Credential) { $parameters["Credential"] = $Credential } + Invoke-Command @parameters + + Write-Message -Level Verbose -Message "[$computer] Accessing computer using PowerShell Remoting - Success!" + $connection.ReportSuccess('PowerShellRemoting') + $connection.AddGoodCredential($cred) + if (-not $disable_cache) { [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$computer] = $connection } + continue main + } + catch { + # Will always consider authenticated, since any call with credentials to a server that doesn't exist will also carry invalid credentials error. + # There simply is no way to differentiate between actual authentication errors and server not reached + $connection.ReportFailure('PowerShellRemoting') + $excluded += "PowerShellRemoting" + continue sub + } + } + #endregion PowerShell Remoting + } + } + } + } + End { + + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaComputerCertificate { + <# + .SYNOPSIS + Simplifies finding computer certificates that are candidates for using with SQL Server's network encryption + + .DESCRIPTION + Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + + .PARAMETER ComputerName + The target SQL Server - defaults to localhost. If target is a cluster, you must specify the distinct nodes. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. + + .PARAMETER Store + Certificate store - defaults to LocalMachine + + .PARAMETER Folder + Certificate folder - defaults to My (Personal) + + .PARAMETER Path + The path to a certificate - basically changes the path into a certificate object + + .PARAMETER Thumbprint + Return certificate based on thumbprint + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Certificate + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaComputerCertificate + Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + + .EXAMPLE + Get-DbaComputerCertificate -ComputerName sql2016 + + Gets computer certificates on sql2016 that are candidates for using with SQL Server's network encryption + + .EXAMPLE + Get-DbaComputerCertificate -ComputerName sql2016 -Thumbprint 8123472E32AB412ED4288888B83811DB8F504DED, 04BFF8B3679BB01A986E097868D8D494D70A46D6 + + Gets computer certificates on sql2016 that match thumbprints 8123472E32AB412ED4288888B83811DB8F504DED or 04BFF8B3679BB01A986E097868D8D494D70A46D6 + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [string]$Path, + [string[]]$Thumbprint, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + #region Scriptblock for remoting + $scriptblock = { + param ( + $Thumbprint, + $Store, + $Folder, + $Path + ) + + if ($Path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $Certificate.Import($bytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) + return $Certificate + } + + if ($Thumbprint) { + try { + Write-Verbose "Searching Cert:\$Store\$Folder" + Get-ChildItem "Cert:\$Store\$Folder" -Recurse | Where-Object Thumbprint -in $Thumbprint + } + catch { + # don't care - there's a weird issue with remoting where an exception gets thrown for no apparent reason + } + } + else { + try { + Write-Verbose "Searching Cert:\$Store\$Folder" + Get-ChildItem "Cert:\$Store\$Folder" -Recurse | Where-Object { "$($_.EnhancedKeyUsageList)" -match '1\.3\.6\.1\.5\.5\.7\.3\.1' } + } + catch { + # still don't care + } + } + } + #endregion Scriptblock for remoting + } + + process { + foreach ($computer in $computername) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $thumbprint, $Store, $Folder, $Path -ErrorAction Stop | Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + catch { + Stop-Function -Message "Issue connecting to computer" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaComputerSystem { + <# + .SYNOPSIS + Gets computer system information from the server. + + .DESCRIPTION + Gets computer system information from the server and returns as an object. + + .PARAMETER ComputerName + Target computer(s). If no computer name is specified, the local computer is targeted + + .PARAMETER Credential + Alternate credential object to use for accessing the target computer(s). + + .PARAMETER IncludeAws + If computer is hosted in AWS Infrastructure as a Service (IaaS), additional information will be included. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ServerInfo + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https: //dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaComputerSystem + + .EXAMPLE + Get-DbaComputerSystem + + Returns information about the local computer's computer system + + .EXAMPLE + Get-DbaComputerSystem -ComputerName sql2016 + + Returns information about the sql2016's computer system + + .EXAMPLE + Get-DbaComputerSystem -ComputerName sql2016 -IncludeAws + + Returns information about the sql2016's computer system and includes additional properties around the EC2 instance. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch]$IncludeAws, + [switch][Alias('Silent')] + $EnableException + ) + process { + foreach ($computer in $ComputerName) { + try { + Write-Message -Level Verbose -Message "Attempting to connect to $computer" + $server = Resolve-DbaNetworkName -ComputerName $computer.ComputerName -Credential $Credential + + $computerResolved = $server.FullComputerName + + if (!$computerResolved) { + Stop-Function -Message "Unable to resolve hostname of $computer. Skipping." -Continue + } + + if (Test-Bound "Credential") { + $computerSystem = Get-DbaCmObject -ClassName Win32_ComputerSystem -ComputerName $computerResolved -Credential $Credential + } + else { + $computerSystem = Get-DbaCmObject -ClassName Win32_ComputerSystem -ComputerName $computerResolved + } + + $adminPasswordStatus = + switch ($computerSystem.AdminPasswordStatus) { + 0 { "Disabled" } + 1 { "Enabled" } + 2 { "Not Implemented" } + 3 { "Unknown" } + default { "Unknown" } + } + + $domainRole = + switch ($computerSystem.DomainRole) { + 0 { "Standalone Workstation" } + 1 { "Member Workstation" } + 2 { "Standalone Server" } + 3 { "Member Server" } + 4 { "Backup Domain Controller" } + 5 { "Primary Domain Controller" } + } + + $isHyperThreading = $false + if ($computerSystem.NumberOfLogicalProcessors -gt $computerSystem.NumberofProcessors) { + $isHyperThreading = $true + } + + if ($IncludeAws) { + $isAws = Invoke-Command2 -ComputerName $computerResolved -Credential $Credential -ScriptBlock { ((Invoke-WebRequest -TimeoutSec 15 -Uri 'http://169.254.169.254').StatusCode) -eq 200 } -Raw + + if ($isAws) { + $scriptBlock = { + [PSCustomObject]@{ + AmiId = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/ami-id').Content + IamRoleArn = ((Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/iam/info').Content | ConvertFrom-Json).InstanceProfileArn + InstanceId = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/instance-id').Content + InstanceType = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/instance-type').Content + AvailabilityZone = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/placement/availability-zone').Content + PublicHostname = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/public-hostname').Content + } + } + $awsProps = Invoke-Command2 -ComputerName $computerResolved -Credential $Credential -ScriptBlock $scriptBlock + } + else { + Write-Message -Level Warning -Message "$computerResolved was not found to be an EC2 instance. Verify http://169.254.169.254 is accessible on the computer." + } + } + $inputObject = [PSCustomObject]@{ + ComputerName = $computerResolved + Domain = $computerSystem.Domain + DomainRole = $domainRole + Manufacturer = $computerSystem.Manufacturer + Model = $computerSystem.Model + SystemFamily = $computerSystem.SystemFamily + SystemSkuNumber = $computerSystem.SystemSKUNumber + SystemType = $computerSystem.SystemType + NumberLogicalProcessors = $computerSystem.NumberOfLogicalProcessors + NumberProcessors = $computerSystem.NumberOfProcessors + IsHyperThreading = $isHyperThreading + TotalPhysicalMemory = [DbaSize]$computerSystem.TotalPhysicalMemory + IsDaylightSavingsTime = $computerSystem.EnableDaylightSavingsTime + DaylightInEffect = $computerSystem.DaylightInEffect + DnsHostName = $computerSystem.DNSHostName + IsSystemManagedPageFile = $computerSystem.AutomaticManagedPagefile + AdminPasswordStatus = $adminPasswordStatus + } + if ($IncludeAws -and $isAws) { + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsAmiId -Value $awsProps.AmiId + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsIamRoleArn -Value $awsProps.IamRoleArn + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsEc2InstanceId -Value $awsProps.InstanceId + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsEc2InstanceType -Value $awsProps.InstanceType + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsAvailabilityZone -Value $awsProps.AvailabilityZone + Add-Member -Force -InputObject $inputObject -MemberType NoteProperty -Name AwsPublicHostName -Value $awsProps.PublicHostname + } + $excludes = 'SystemSkuNumber', 'IsDaylightSavingsTime', 'DaylightInEffect', 'DnsHostName', 'AdminPasswordStatus' + Select-DefaultView -InputObject $inputObject -ExcludeProperty $excludes + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +function Get-DbaConfig { + <# + .SYNOPSIS + Retrieves configuration elements by name. + + .DESCRIPTION + Retrieves configuration elements by name. + Can be used to search the existing configuration list. + + .PARAMETER FullName + Default: "*" + Search for configurations using the full name + + .PARAMETER Name + Default: "*" + The name of the configuration element(s) to retrieve. + May be any string, supports wildcards. + + .PARAMETER Module + Default: "*" + Search configuration by module. + + .PARAMETER Force + Overrides the default behavior and also displays hidden configuration values. + + .NOTES + Tags: Config, Module + Author: Friedrich Weinmann + + .EXAMPLE + PS C:\> Get-DbaConfig 'Mail.To' + + Retrieves the configuration element for the key "Mail.To" + + .EXAMPLE + PS C:\> Get-DbaConfig -Force + + Retrieve all configuration elements from all modules, even hidden ones. + #> + [CmdletBinding(DefaultParameterSetName = "FullName")] + Param ( + [Parameter(ParameterSetName = "FullName", Position = 0)] + [string] + $FullName = "*", + + [Parameter(ParameterSetName = "Module", Position = 1)] + [string] + $Name = "*", + + [Parameter(ParameterSetName = "Module", Position = 0)] + [string] + $Module = "*", + + [switch] + $Force + ) + + switch ($PSCmdlet.ParameterSetName) { + "Module" { + $Name = $Name.ToLower() + $Module = $Module.ToLower() + + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { ($_.Name -like $Name) -and ($_.Module -like $Module) -and ((-not $_.Hidden) -or ($Force)) } | Sort-Object Module, Name + } + + "FullName" { + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { ("$($_.Module).$($_.Name)" -like $FullName) -and ((-not $_.Hidden) -or ($Force)) } | Sort-Object Module, Name + } + } +} +function Get-DbaConfigValue { + <# + .SYNOPSIS + Returns the configuration value stored under the specified name. + + .DESCRIPTION + Returns the configuration value stored under the specified name. + It requires the full name (.) and is usually only called by functions. + + .PARAMETER FullName + The full name (.) of the configured value to return. + + .PARAMETER Fallback + A fallback value to use, if no value was registered to a specific configuration element. + This basically is a default value that only applies on a "per call" basis, rather than a system-wide default. + + .PARAMETER NotNull + By default, this function returns null if one tries to retrieve the value from either a Configuration that does not exist or a Configuration whose value was set to null. + However, sometimes it may be important that some value was returned. + By specifying this parameter, the function will throw an error if no value was found at all. + + .EXAMPLE + PS C:\> Get-DbaConfigValue -Name 'System.MailServer' + + Returns the configured value that was assigned to the key 'System.MailServer' + + .EXAMPLE + PS C:\> Get-DbaConfigValue -Name 'Default.CoffeeMilk' -Fallback 0 + + Returns the configured value for 'Default.CoffeeMilk'. If no such value is configured, it returns '0' instead. + + .NOTES + Author: Friedrich Weinmann + Tags: Config + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "")] + [CmdletBinding()] + Param ( + [Alias('Name')] + [Parameter(Mandatory = $true)] + [string] + $FullName, + + [object] + $Fallback, + + [switch] + $NotNull + ) + + $FullName = $FullName.ToLower() + + $temp = $null + $temp = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$FullName].Value + if ($temp -eq $null) { $temp = $Fallback } + + if ($NotNull -and ($temp -eq $null)) { + Stop-Function -Message "No Configuration Value available for $Name" -EnableException $true -Category InvalidData -Target $FullName + } + else { + return $temp + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaConnection { + <# + .SYNOPSIS + Returns a bunch of information from dm_exec_connections. + + .DESCRIPTION + Returns a bunch of information from dm_exec_connections which, according to Microsoft: + "Returns information about the connections established to this instance of SQL Server and the details of each connection. Returns server wide connection information for SQL Server. Returns current database connection information for SQL Database." + + .PARAMETER SqlInstance + The target SQL Server instance. Server(s) must be SQL Server 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Connection + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaConnection + + .EXAMPLE + Get-DbaConnection -SqlInstance sql2016, sql2017 + + Returns client connection information from sql2016 and sql2017 + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential", "Cred")] + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + begin { + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + session_id as SessionId, most_recent_session_id as MostRecentSessionId, connect_time as ConnectTime, + net_transport as Transport, protocol_type as ProtocolType, protocol_version as ProtocolVersion, + endpoint_id as EndpointId, encrypt_option as EncryptOption, auth_scheme as AuthScheme, node_affinity as NodeAffinity, + num_reads as Reads, num_writes as Writes, last_read as LastRead, last_write as LastWrite, + net_packet_size as PacketSize, client_net_address as ClientNetworkAddress, client_tcp_port as ClientTcpPort, + local_net_address as ServerNetworkAddress, local_tcp_port as ServerTcpPort, connection_id as ConnectionId, + parent_connection_id as ParentConnectionId, most_recent_sql_handle as MostRecentSqlHandle + FROM sys.dm_exec_connections" + } + + process { + foreach ($instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Debug -Message "Getting results for the following query: $sql." + try { + $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -Exception $_ -Continue + } + } + } +} +function Get-DbaCpuUsage { +<# + .SYNOPSIS + Provides detailed CPU usage information about a SQL Server's process + + .DESCRIPTION + "If there are a lot of processes running on your instance and the CPU is very high, + then it's hard to find the exact process eating up your CPU using just the SQL Server + tools. One way to correlate the data between what is running within SQL Server and at + the Windows level is to use SPID and KPID values to get the exact process." + + This command automates that process. + + References: https://www.mssqltips.com/sqlservertip/2454/how-to-find-out-how-much-cpu-a-sql-server-process-is-really-using/ + + Note: This command returns results from all SQL instances on the destionation server but the process + column is specific to -SqlInstance passed. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Allows you to login to the SQL instance using alternative credentials. + + .PARAMETER Credential + Allows you to login to the Windows Server using alternative credentials. + + .PARAMETER Threshold + CPU threshold. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CPU + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaCpuUsage + + .EXAMPLE + Get-DbaCpuUsage -SqlInstance sql2017 + + Logs into the SQL Server instance "sql2017" and also the Computer itself (via WMI) to gather information + + .EXAMPLE + $usage = Get-DbaCpuUsage -SqlInstance sql2017 + $usage.Process + + Explores the processes (from Get-DbaProcess) associated with the usage results + + .EXAMPLE + Get-DbaCpuUsage -SqlInstance sql2017 -SqlCredential (Get-Credential sqladmin) -Credential (Get-Credential ad\sqldba) + + Logs into the SQL instance using the SQL Login 'sqladmin' and then Windows instance as 'ad\sqldba' +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [int]$Threshold = 0, + [switch]$EnableException + ) + begin { + # This can likely be enumerated but I don't know hows + $threadstates = [pscustomobject]@{ + 0 = 'Initialized. It is recognized by the microkernel.' + 1 = 'Ready. It is prepared to run on the next available processor.' + 2 = 'Running. It is executing.' + 3 = 'Standby. It is about to run. Only one thread may be in this state at a time.' + 4 = 'Terminated. It is finished executing.' + 5 = 'Waiting. It is not ready for the processor. When ready, it will be rescheduled.' + 6 = 'Transition. The thread is waiting for resources other than the processor.' + 7 = 'Unknown. The thread state is unknown.' + } + + $threadwaitreasons = [pscustomobject]@{ + 0 = 'Executive' + 1 = 'FreePage' + 2 = 'PageIn' + 3 = 'PoolAllocation' + 4 = 'ExecutionDelay' + 5 = 'FreePage' + 6 = 'PageIn' + 7 = 'Executive' + 8 = 'FreePage' + 9 = 'PageIn' + 10 = 'PoolAllocation' + 11 = 'ExecutionDelay' + 12 = 'FreePage' + 13 = 'PageIn' + 14 = 'EventPairHigh' + 15 = 'EventPairLow' + 16 = 'LPCReceive' + 17 = 'LPCReply' + 18 = 'VirtualMemory' + 19 = 'PageOut' + 20 = 'Unknown' + } + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $processes = Get-DbaProcess -SqlInstance $server + $threads = Get-DbaCmObject -ComputerName $instance.ComputerName -ClassName Win32_PerfFormattedData_PerfProc_Thread -Credential $Credential | Where-Object { $_.Name -like 'sql*' -and $_.PercentProcessorTime -ge $Threshold } + + if ($server.VersionMajor -eq 8) { + $spidcollection = $server.Query("select spid, kpid from sysprocesses") + } + else { + $spidcollection = $server.Query("select t.os_thread_id as kpid, s.session_id as spid + from sys.dm_exec_sessions s + join sys.dm_exec_requests er on s.session_id = er.session_id + join sys.dm_os_workers w on er.task_address = w.task_address + join sys.dm_os_threads t on w.thread_address = t.thread_address") + } + + foreach ($thread in $threads) { + $spid = ($spidcollection | Where-Object kpid -eq $thread.IDThread).spid + $process = $processes | Where-Object spid -eq $spid + $threadwaitreason = $thread.ThreadWaitReason + $threadstate = $thread.ThreadState + $ThreadStateValue = $threadstates.$threadstate + $ThreadWaitReasonValue = $threadwaitreasons.$threadwaitreason + + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name Processes -Value ($processes | Where-Object HostProcessID -eq $thread.IDProcess) + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name ThreadStateValue -Value $ThreadStateValue + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name ThreadWaitReasonValue -Value $ThreadWaitReasonValue + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name Process -Value $process + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name Query -Value $process.LastQuery + Add-Member -Force -InputObject $thread -MemberType NoteProperty -Name Spid -Value $spid + + Select-DefaultView -InputObject $thread -Property ComputerName, InstanceName, SqlInstance, Name, ContextSwitchesPersec, ElapsedTime, IDProcess, Spid, PercentPrivilegedTime, PercentProcessorTime, PercentUserTime, PriorityBase, PriorityCurrent, StartAddress, ThreadStateValue, ThreadWaitReasonValue, Process, Query + } + } + } +} +#ValidationTags#Messaging,FlowControl,CodeStyle# +function Get-DbaCredential { + <# + .SYNOPSIS + Gets SQL Credential information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaCredential command gets SQL Credential information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + Only include specific names + Note: if spaces exist in the credential name, you will have to type "" or '' around it. + + .PARAMETER ExcludeName + Excluded credential names + + .PARAMETER Identity + Only include specific identities + Note: if spaces exist in the credential identity, you will have to type "" or '' around it. + + .PARAMETER ExcludeIdentity + Excluded identities + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Credential + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaCredential + + .EXAMPLE + Get-DbaCredential -SqlInstance localhost + + Returns all SQL Credentials on the local default SQL Server instance + + .EXAMPLE + Get-DbaCredential -SqlInstance localhost, sql2016 -Name 'PowerShell Proxy' + + Returns the SQL Credentials named 'PowerShell Proxy' for the local and sql2016 SQL Server instances + + .EXAMPLE + Get-DbaCredential -SqlInstance localhost, sql2016 -Identity ad\powershell + + Returns the SQL Credentials for the account 'ad\powershell' on the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Name, + [string[]]$ExcludeName, + [Alias('CredentialIdentity')] + [string[]]$Identity, + [Alias('ExcludeCredentialIdentity')] + [string[]]$ExcludeIdentity, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $credential = $server.Credentials + + if ($Name) { + $credential = $credential | Where-Object { $Name -contains $_.Name } + } + + if ($ExcludeName) { + $credential = $credential | Where-Object { $ExcludeName -notcontains $_.Name } + } + + if ($Identity) { + $credential = $credential | Where-Object { $Identity -contains $_.Identity } + } + + if ($ExcludeIdentity) { + $credential = $credential | Where-Object { $ExcludeIdentity -notcontains $_.Identity } + } + + foreach ($currentcredential in $credential) { + Add-Member -Force -InputObject $currentcredential -MemberType NoteProperty -Name ComputerName -value $currentcredential.Parent.NetName + Add-Member -Force -InputObject $currentcredential -MemberType NoteProperty -Name InstanceName -value $currentcredential.Parent.ServiceName + Add-Member -Force -InputObject $currentcredential -MemberType NoteProperty -Name SqlInstance -value $currentcredential.Parent.DomainInstanceName + + Select-DefaultView -InputObject $currentcredential -Property ComputerName, InstanceName, SqlInstance, ID, Name, Identity, MappedClassType, ProviderName + } + } + } +} +#ValidationTags#Messaging,FlowControl,CodeStyle# +function Get-DbaCustomError { + <# + .SYNOPSIS + Gets SQL Custom Error Message information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaCustomError command gets SQL Custom Error Message information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CustomError + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaCustomError + + .EXAMPLE + Get-DbaCustomError -SqlInstance localhost + + Returns all Custom Error Message(s) on the local default SQL Server instance + + .EXAMPLE + Get-DbaCustomError -SqlInstance localhost, sql2016 + + Returns all Custom Error Message(s) for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($customError in $server.UserDefinedMessages) { + Add-Member -Force -InputObject $customError -MemberType NoteProperty -Name ComputerName -value $customError.Parent.NetName + Add-Member -Force -InputObject $customError -MemberType NoteProperty -Name InstanceName -value $customError.Parent.ServiceName + Add-Member -Force -InputObject $customError -MemberType NoteProperty -Name SqlInstance -value $customError.Parent.DomainInstanceName + + Select-DefaultView -InputObject $customError -Property ComputerName, InstanceName, SqlInstance, ID, Text, LanguageID, Language + } + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaDatabase { + <# + .SYNOPSIS + Gets SQL Database information for each database that is present on the target instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaDatabase command gets SQL database information for each database that is present on the target instance(s) of + SQL Server. If the name of the database is provided, the command will return only the specific database information. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER ExcludeAllUserDb + If this switch is enabled, only databases which are not User databases will be processed. + + This parameter cannot be used with -ExcludeAllSystemDb. + + .PARAMETER ExcludeAllSystemDb + If this switch is enabled, only databases which are not System databases will be processed. + + This parameter cannot be used with -ExcludeAllUserDb. + + .PARAMETER Status + Specifies one or more database statuses to filter on. Only databases in the status(es) listed will be returned. Valid options for this parameter are 'Emergency', 'Normal', 'Offline', 'Recovering', 'Restoring', 'Standby', and 'Suspect'. + + .PARAMETER Access + Filters databases returned by their access type. Valid options for this parameter are 'ReadOnly' and 'ReadWrite'. If omitted, no filtering is performed. + + .PARAMETER Owner + Specifies one or more database owners. Only databases owned by the listed owner(s) will be returned. + + .PARAMETER Encrypted + If this switch is enabled, only databases which have Transparent Data Encryption (TDE) enabled will be returned. + + .PARAMETER RecoveryModel + Filters databases returned by their recovery model. Valid options for this parameter are 'Full', 'Simple', and 'BulkLogged'. + + .PARAMETER NoFullBackup + If this switch is enabled, only databases without a full backup recorded by SQL Server will be returned. This will also indicate which of these databases only have CopyOnly full backups. + + .PARAMETER NoFullBackupSince + Only databases which haven't had a full backup since the specified DateTime will be returned. + + .PARAMETER NoLogBackup + If this switch is enabled, only databases without a log backup recorded by SQL Server will be returned. This will also indicate which of these databases only have CopyOnly log backups. + + .PARAMETER NoLogBackupSince + Only databases which haven't had a log backup since the specified DateTime will be returned. + + .PARAMETER IncludeLastUsed + If this switch is enabled, the last used read & write times for each database will be returned. This data is retrieved from sys.dm_db_index_usage_stats which is reset when SQL Server is restarted. + + .PARAMETER OnlyAccessible + If this switch is enabled, only accessible databases are returned (huge speedup in SMO enumeration) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Author: Garry Bargsley (@gbargsley | http://blog.garrybargsley.com) + Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + Author: Simone Bizzotto ( @niphlod ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDatabase + + .EXAMPLE + Get-DbaDatabase -SqlInstance localhost + + Returns all databases on the local default SQL Server instance. + + .EXAMPLE + Get-DbaDatabase -SqlInstance localhost -ExcludeAllUserDb + + Returns only the system databases on the local default SQL Server instance. + + .EXAMPLE + Get-DbaDatabase -SqlInstance localhost -ExcludeAllSystemDb + + Returns only the user databases on the local default SQL Server instance. + + .EXAMPLE + 'localhost','sql2016' | Get-DbaDatabase + + Returns databases on multiple instances piped into the function. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress -RecoveryModel full,Simple + + Returns only the user databases in Full or Simple recovery model from SQL Server instance SQL1\SQLExpress. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress -Status Normal + + Returns only the user databases with status 'normal' from SQL Server instance SQL1\SQLExpress. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress -IncludeLastUsed + + Returns the databases from SQL Server instance SQL1\SQLExpress and includes the last used information + from the sys.dm_db_index_usage_stats DMV. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress,SQL2 -ExcludeDatabase model,master + + Returns all databases except master and model from SQL Server instances SQL1\SQLExpress and SQL2. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress,SQL2 -Encrypted + + Returns only databases using TDE from SQL Server instances SQL1\SQLExpress and SQL2. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL1\SQLExpress,SQL2 -Access ReadOnly + + Returns only read only databases from SQL Server instances SQL1\SQLExpress and SQL2. + + .EXAMPLE + Get-DbaDatabase -SqlInstance SQL2,SQL3 -Database OneDB,OtherDB + + Returns databases 'OneDb' and 'OtherDB' from SQL Server instances SQL2 and SQL3 if databases by those names exist on those instances. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias("SystemDbOnly", "NoUserDb")] + [switch]$ExcludeAllUserDb, + [Alias("UserDbOnly", "NoSystemDb")] + [switch]$ExcludeAllSystemDb, + [string[]]$Owner, + [switch]$Encrypted, + [ValidateSet('EmergencyMode', 'Normal', 'Offline', 'Recovering', 'Restoring', 'Standby', 'Suspect')] + [string[]]$Status = @('EmergencyMode', 'Normal', 'Offline', 'Recovering', 'Restoring', 'Standby', 'Suspect'), + [ValidateSet('ReadOnly', 'ReadWrite')] + [string]$Access, + [ValidateSet('Full', 'Simple', 'BulkLogged')] + [string[]]$RecoveryModel = @('Full', 'Simple', 'BulkLogged'), + [switch]$NoFullBackup, + [datetime]$NoFullBackupSince, + [switch]$NoLogBackup, + [datetime]$NoLogBackupSince, + [Alias('Silent')] + [switch]$EnableException, + [switch]$IncludeLastUsed, + [switch]$OnlyAccessible + ) + + begin { + + if ($ExcludeAllUserDb -and $ExcludeAllSystemDb) { + Stop-Function -Message "You cannot specify both ExcludeAllUserDb and ExcludeAllSystemDb." -Continue -EnableException $EnableException + } + + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (!$IncludeLastUsed) { + $dblastused = $null + } + else { + ## Get last used information from the DMV + $querylastused = "WITH agg AS + ( + SELECT + max(last_user_seek) last_user_seek, + max(last_user_scan) last_user_scan, + max(last_user_lookup) last_user_lookup, + max(last_user_update) last_user_update, + sd.name dbname + FROM + sys.dm_db_index_usage_stats, master..sysdatabases sd + WHERE + database_id = sd.dbid AND database_id > 4 + group by sd.name + ) + SELECT + dbname, + last_read = MAX(last_read), + last_write = MAX(last_write) + FROM + ( + SELECT dbname, last_user_seek, NULL FROM agg + UNION ALL + SELECT dbname, last_user_scan, NULL FROM agg + UNION ALL + SELECT dbname, last_user_lookup, NULL FROM agg + UNION ALL + SELECT dbname, NULL, last_user_update FROM agg + ) AS x (dbname, last_read, last_write) + GROUP BY + dbname + ORDER BY 1;" + # put a function around this to enable Pester Testing and also to ease any future changes + function Invoke-QueryDBlastUsed { + $server.Query($querylastused) + } + $dblastused = Invoke-QueryDBlastUsed + } + + if ($ExcludeAllUserDb) { + $DBType = @($true) + } + elseif ($ExcludeAllSystemDb) { + $DBType = @($false) + } + else { + $DBType = @($false, $true) + } + + $AccessibleFilter = switch ($OnlyAccessible) { + $true { @($true) } + default { @($true, $false) } + } + + $Readonly = switch ($Access) { + 'Readonly' { @($true) } + 'ReadWrite' { @($false) } + default { @($true, $false) } + } + $Encrypt = switch (Test-Bound $Encrypted) { + $true { @($true) } + default { @($true, $false, $null) } + } + function Invoke-QueryRawDatabases { + if ($server.VersionMajor -eq 8) { + $server.Query("SELECT *, SUSER_NAME(sid) AS [Owner] FROM master.dbo.sysdatabases") + } + else { + $server.Query("SELECT *, SUSER_NAME(owner_sid) AS [Owner] FROM sys.databases") + } + } + $backed_info = Invoke-QueryRawDatabases + $backed_info = $backed_info | Where-Object { + ($_.name -in $Database -or !$Database) -and + ($_.name -notin $ExcludeDatabase -or !$ExcludeDatabase) -and + ($_.Owner -in $Owner -or !$Owner) -and + ($_.state -ne 6 -or !$OnlyAccessible) + } + + $inputObject = @() + foreach($dt in $backed_info) { + $inputObject += $server.Databases[$dt.name] + } + $inputobject = $inputObject | + Where-Object { + ($_.Name -in $Database -or !$Database) -and + ($_.Name -notin $ExcludeDatabase -or !$ExcludeDatabase) -and + ($_.Owner -in $Owner -or !$Owner) -and + $_.ReadOnly -in $Readonly -and + $_.IsAccessible -in $AccessibleFilter -and + $_.IsSystemObject -in $DBType -and + ((Compare-Object @($_.Status.tostring().split(',').trim()) $Status -ExcludeDifferent -IncludeEqual).inputobject.count -ge 1 -or !$status) -and + $_.RecoveryModel -in $RecoveryModel -and + $_.EncryptionEnabled -in $Encrypt + } + if ($NoFullBackup -or $NoFullBackupSince) { + $dabs = (Get-DbaBackupHistory -SqlInstance $server -LastFull ) + if ($null -ne $NoFullBackupSince) { + $dabsWithinScope = ($dabs | Where-Object End -lt $NoFullBackupSince) + + $inputobject = $inputobject | Where-Object { $_.Name -in $dabsWithinScope.Database -and $_.Name -ne 'tempdb' } + } + else { + $inputObject = $inputObject | Where-Object { $_.Name -notin $dabs.Database -and $_.Name -ne 'tempdb' } + } + + } + if ($NoLogBackup -or $NoLogBackupSince) { + $dabs = (Get-DbaBackupHistory -SqlInstance $server -LastLog ) + if ($null -ne $NoLogBackupSince) { + $dabsWithinScope = ($dabs | Where-Object End -lt $NoLogBackupSince) + $inputobject = $inputobject | + Where-Object { $_.Name -in $dabsWithinScope.Database -and $_.Name -ne 'tempdb' -and $_.RecoveryModel -ne 'Simple' } + } + else { + $inputobject = $inputObject | + Where-Object { $_.Name -notin $dabs.Database -and $_.Name -ne 'tempdb' -and $_.RecoveryModel -ne 'Simple' } + } + } + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name', 'Status', 'IsAccessible', 'RecoveryModel', + 'LogReuseWaitStatus', 'Size as SizeMB', 'CompatibilityLevel as Compatibility', 'Collation', 'Owner', + 'LastBackupDate as LastFullBackup', 'LastDifferentialBackupDate as LastDiffBackup', + 'LastLogBackupDate as LastLogBackup' + + if ($NoFullBackup -or $NoFullBackupSince -or $NoLogBackup -or $NoLogBackupSince) { + $defaults += ('Notes') + } + if ($IncludeLastUsed) { + # Add Last Used to the default view + $defaults += ('LastRead as LastIndexRead', 'LastWrite as LastIndexWrite') + } + + try { + foreach ($db in $inputobject) { + + $Notes = $null + if ($NoFullBackup -or $NoFullBackupSince) { + if (@($db.EnumBackupSets()).count -eq @($db.EnumBackupSets() | Where-Object { $_.IsCopyOnly }).count -and (@($db.EnumBackupSets()).count -gt 0)) { + $Notes = "Only CopyOnly backups" + } + } + + $lastusedinfo = $dblastused | Where-Object { $_.dbname -eq $db.name } + Add-Member -Force -InputObject $db -MemberType NoteProperty BackupStatus -value $Notes + Add-Member -Force -InputObject $db -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $db -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $db -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $db -MemberType NoteProperty -Name LastRead -value $lastusedinfo.last_read + Add-Member -Force -InputObject $db -MemberType NoteProperty -Name LastWrite -value $lastusedinfo.last_write + Select-DefaultView -InputObject $db -Property $defaults + #try { $server.Databases.Refresh() } catch {} + } + } + catch { + Stop-Function -ErrorRecord $_ -Target $instance -Message "Failure. Collection may have been modified. If so, please use parens (Get-DbaDatabase ....) | when working with commands that modify the collection such as Remove-DbaDatabase." -Continue + } + } + } +} +#ValidationTags#Messaging,FlowControl,CodeStyle# +function Get-DbaDatabaseAssembly { + <# + .SYNOPSIS + Gets SQL Database Assembly information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaDatabaseAssembly command gets SQL Database Assembly information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Assembly, Database + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDatabaseAssembly + + .EXAMPLE + Get-DbaDatabaseAssembly -SqlInstance localhost + + Returns all Database Assembly on the local default SQL Server instance + + .EXAMPLE + Get-DbaDatabaseAssembly -SqlInstance localhost, sql2016 + + Returns all Database Assembly for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($database in ($server.Databases | Where-Object IsAccessible)) { + try { + foreach ($assembly in $database.assemblies) { + + Add-Member -Force -InputObject $assembly -MemberType NoteProperty -Name ComputerName -value $assembly.Parent.Parent.NetName + Add-Member -Force -InputObject $assembly -MemberType NoteProperty -Name InstanceName -value $assembly.Parent.Parent.ServiceName + Add-Member -Force -InputObject $assembly -MemberType NoteProperty -Name SqlInstance -value $assembly.Parent.Parent.DomainInstanceName + + Select-DefaultView -InputObject $assembly -Property ComputerName, InstanceName, SqlInstance, ID, Name, Owner, 'AssemblySecurityLevel as SecurityLevel', CreateDate, IsSystemObject, Version + } + } + catch { + Stop-Function -Message "Issue pulling assembly information" -Target $assembly -ErrorRecord $_ -Continue + } + } + } + } +} +function Get-DbaDatabaseEncryption { + <# + .SYNOPSIS + Returns a summary of encryption used on databases passed to it. + + .DESCRIPTION + Shows if a database has Transparent Data Encryption (TDE), any certificates, asymmetric keys or symmetric keys with details for each. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server. + + .PARAMETER IncludeSystemDBs + Switch parameter that when used will display system database information. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Encryption, Database + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDatabaseEncryption + + .EXAMPLE + Get-DbaDatabaseEncryption -SqlInstance DEV01 + + List all encryption found on the instance by database + + .EXAMPLE + Get-DbaDatabaseEncryption -SqlInstance DEV01 -Database MyDB + + List all encryption found for the MyDB database. + + .EXAMPLE + Get-DbaDatabaseEncryption -SqlInstance DEV01 -ExcludeDatabase MyDB + + List all encryption found for all databases except MyDB. + + .EXAMPLE + Get-DbaDatabaseEncryption -SqlInstance DEV01 -IncludeSystemDBs + + List all encryption found for all databases including the system databases. + #> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystemDBs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + #For each SQL Server in collection, connect and get SMO object + Write-Message -Level Verbose -Message "Connecting to $instance" + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + #If IncludeSystemDBs is true, include systemdbs + #only look at online databases (Status equal normal) + try { + if ($Database) { + $dbs = $server.Databases | Where-Object Name -In $Database + } + elseif ($IncludeSystemDBs) { + $dbs = $server.Databases | Where-Object IsAccessible + } + else { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.IsSystemObject -eq 0 } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + } + catch { + Stop-Function -Message "Unable to gather dbs for $instance" -Target $instance -Continue + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db" + + if ($db.EncryptionEnabled -eq $true) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Encryption = "EncryptionEnabled (TDE)" + Name = $null + LastBackup = $null + PrivateKeyEncryptionType = $null + EncryptionAlgorithm = $null + KeyLength = $null + Owner = $null + Object = $null + ExpirationDate = $null + } + + } + + foreach ($cert in $db.Certificates) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Encryption = "Certificate" + Name = $cert.Name + LastBackup = $cert.LastBackupDate + PrivateKeyEncryptionType = $cert.PrivateKeyEncryptionType + EncryptionAlgorithm = $null + KeyLength = $null + Owner = $cert.Owner + Object = $cert + ExpirationDate = $cert.ExpirationDate + } + + } + + foreach ($ak in $db.AsymmetricKeys) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Encryption = "Asymmetric key" + Name = $ak.Name + LastBackup = $null + PrivateKeyEncryptionType = $ak.PrivateKeyEncryptionType + EncryptionAlgorithm = $ak.KeyEncryptionAlgorithm + KeyLength = $ak.KeyLength + Owner = $ak.Owner + Object = $ak + ExpirationDate = $null + } + + } + foreach ($sk in $db.SymmetricKeys) { + [PSCustomObject]@{ + Server = $server.name + Instance = $server.InstanceName + Database = $db.Name + Encryption = "Symmetric key" + Name = $sk.Name + LastBackup = $null + PrivateKeyEncryptionType = $sk.PrivateKeyEncryptionType + EncryptionAlgorithm = $ak.EncryptionAlgorithm + KeyLength = $sk.KeyLength + Owner = $sk.Owner + Object = $sk + ExpirationDate = $null + } + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaDatabaseFile { + <# + .SYNOPSIS + Returns detailed information about database files. + + .DESCRIPTION + Returns detailed information about database files. Does not use SMO - SMO causes enumeration and this command avoids that. + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Credentials to connect to the SQL Server instance if the calling user doesn't have permission + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER InputObject + A piped collection of database objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Stuart Moore (@napalmgram), stuart-moore.com + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDatabaseFile -SqlInstance sql2016 + + Will return an object containing all filegroups and their contained files for every database on the sql2016 SQL Server instance + + .EXAMPLE + Get-DbaDatabaseFile -SqlInstance sql2016 -Database Impromptu + + Will return an object containing all filegroups and their contained files for the Impromptu Database on the sql2016 SQL Server instance + + .EXAMPLE + Get-DbaDatabaseFile -SqlInstance sql2016 -Database Impromptu, Trading + + Will return an object containing all filegroups and their contained files for the Impromptu and Trading databases on the sql2016 SQL Server instance + + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(ParameterSetName = "Pipe", Mandatory, ValueFromPipeline)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $sql = "select + fg.name as FileGroupName, + df.file_id as 'ID', + df.Type, + df.type_desc as TypeDescription, + df.name as LogicalName, + mf.physical_name as PhysicalName, + df.state_desc as State, + df.max_size as MaxSize, + case mf.is_percent_growth when 1 then df.growth else df.Growth*8 end as Growth, + fileproperty(df.name, 'spaceused') as UsedSpace, + df.size as Size, + vfs.size_on_disk_bytes as size_on_disk_bytes, + case df.state_desc when 'OFFLINE' then 'True' else 'False' End as IsOffline, + case mf.is_read_only when 1 then 'True' when 0 then 'False' End as IsReadOnly, + case mf.is_media_read_only when 1 then 'True' when 0 then 'False' End as IsReadOnlyMedia, + case mf.is_sparse when 1 then 'True' when 0 then 'False' End as IsSparse, + case mf.is_percent_growth when 1 then 'Percent' when 0 then 'kb' End as GrowthType, + case mf.is_read_only when 1 then 'True' when 0 then 'False' End as IsReadOnly, + vfs.num_of_writes as NumberOfDiskWrites, + vfs.num_of_reads as NumberOfDiskReads, + vfs.num_of_bytes_read as BytesReadFromDisk, + vfs.num_of_bytes_written as BytesWrittenToDisk, + fg.data_space_id as FileGroupDataSpaceId, + fg.Type as FileGroupType, + fg.type_desc as FileGroupTypeDescription, + case fg.is_default When 1 then 'True' when 0 then 'False' end as FileGroupDefault, + fg.is_read_only as FileGroupReadOnly" + + $sqlfrom = "from sys.database_files df + left outer join sys.filegroups fg on df.data_space_id=fg.data_space_id + inner join sys.dm_io_virtual_file_stats(db_id(),NULL) vfs on df.file_id=vfs.file_id + inner join sys.master_files mf on df.file_id = mf.file_id + and mf.database_id = db_id()" + + $sql2008 = ",vs.available_bytes as 'VolumeFreeSpace'" + $sql2008from = "cross apply sys.dm_os_volume_stats(db_id(),df.file_id) vs" + + $sql2000 = "select + fg.groupname as FileGroupName, + df.fileid as ID, + CONVERT(INT,df.status & 0x40) / 64 as Type, + case CONVERT(INT,df.status & 0x40) / 64 when 1 then 'LOG' else 'ROWS' end as TypeDescription, + df.name as LogicalName, + df.filename as PhysicalName, + 'Existing' as State, + df.maxsize as MaxSize, + case CONVERT(INT,df.status & 0x100000) / 1048576 when 1 then df.growth when 0 then df.growth*8 End as Growth, + fileproperty(df.name, 'spaceused') as UsedSpace, + df.size as Size, + case CONVERT(INT,df.status & 0x20000000) / 536870912 when 1 then 'True' else 'False' End as IsOffline, + case CONVERT(INT,df.status & 0x10) / 16 when 1 then 'True' when 0 then 'False' End as IsReadOnly, + case CONVERT(INT,df.status & 0x1000) / 4096 when 1 then 'True' when 0 then 'False' End as IsReadOnlyMedia, + case CONVERT(INT,df.status & 0x10000000) / 268435456 when 1 then 'True' when 0 then 'False' End as IsSparse, + case CONVERT(INT,df.status & 0x100000) / 1048576 when 1 then 'Percent' when 0 then 'kb' End as GrowthType, + case CONVERT(INT,df.status & 0x1000) / 4096 when 1 then 'True' when 0 then 'False' End as IsReadOnly, + fg.groupid as FileGroupDataSpaceId, + NULL as FileGroupType, + NULL AS FileGroupTypeDescription, + CAST(fg.Status & 0x10 as BIT) as FileGroupDefault, + CAST(fg.Status & 0x8 as BIT) as FileGroupReadOnly + from sysfiles df + left outer join sysfilegroups fg on df.groupid=fg.groupid" + + if ($Database) { + $InputObject = $server.Databases | Where-Object Name -in $database + } + else { + $InputObject = $server.Databases + } + + if ($ExcludeDatabase) { + $InputObject = $InputObject | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $InputObject) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping" + continue + } + Write-Message -Level Verbose -Message "Querying database $db" + + $version = Test-DbaDatabaseCompatibility -SqlInstance $server -Database $db.Name | Select-Object DatabaseCompatibility + $version = + ($version.DatabaseCompatibility.ToString().replace("Version", "")) / 10 + + if ($version -ge 11) { + $query = ($sql, $sql2008, $sqlfrom, $sql2008from) -Join "`n" + } + elseif ($version -ge 9) { + $query = ($sql, $sqlfrom) -Join "`n" + } + else { + $query = $sql2000 + } + + Write-Message -Level Debug -Message "SQL Statement: $query" + + $results = $server.Query($query, $db.Name) + + foreach ($result in $results) { + $size = [dbasize]($result.Size * 8192) + $usedspace = [dbasize]($result.UsedSpace * 8192) + $maxsize = $result.MaxSize + # calculation is done here because for snapshots or sparse files size is not the "virtual" size + # (master_files.Size) but the currently allocated one (dm_io_virtual_file_stats.size_on_disk_bytes) + $AvailableSpace = $size - $usedspace + if ($result.size_on_disk_bytes) { + $size = [dbasize]($result.size_on_disk_bytes) + } + if ($maxsize -gt -1) { + $maxsize = [dbasize]($result.MaxSize * 8192) + } + else { + $maxsize = [dbasize]($result.MaxSize) + } + + if ($result.VolumeFreeSpace) { + $VolumeFreeSpace = [dbasize]$result.VolumeFreeSpace + } + else { + # to get drive free space for each drive that a database has files on + # when database compatibility lower than 110. Lets do this with query2 + $query2 = @' +-- to get drive free space for each drive that a database has files on +DECLARE @FixedDrives TABLE(Drive CHAR(1), MB_Free BIGINT); +INSERT @FixedDrives EXEC sys.xp_fixeddrives; + +SELECT DISTINCT fd.MB_Free, LEFT(df.physical_name, 1) AS [Drive] +FROM @FixedDrives AS fd +INNER JOIN sys.database_files AS df +ON fd.Drive = LEFT(df.physical_name, 1); +'@ + # if the server has one drive xp_fixeddrives returns one row, but we still need $disks to be an array. + $disks = @($server.Query($query2, $db.Name)) + $MbFreeColName = $disks[0].psobject.Properties.Name + # get the free MB value for the drive in question + $free = $disks | Where-Object { $_.drive -eq $result.PhysicalName.Substring(0, 1) } | Select-Object $MbFreeColName + + $VolumeFreeSpace = [dbasize](($free.MB_Free) * 1024 * 1024) + } + if ($result.GrowthType -eq "Percent") { + $nextgrowtheventadd = [dbasize]($result.size * ($result.Growth * 0.01) * 1024) + } + else { + $nextgrowtheventadd = [dbasize]($result.Growth * 8 * 1024) + } + if ( ($nextgrowtheventadd.Byte -gt ($MaxSize.Byte - $size.Byte)) -and $maxsize -gt 0 ) { [dbasize]$nextgrowtheventadd = 0 } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + FileGroupName = $result.FileGroupName + ID = $result.ID + Type = $result.Type + TypeDescription = $result.TypeDescription + LogicalName = $result.LogicalName.Trim() + PhysicalName = $result.PhysicalName.Trim() + State = $result.State + MaxSize = $maxsize + Growth = $result.Growth + GrowthType = $result.GrowthType + NextGrowthEventSize = $nextgrowtheventadd + Size = $size + UsedSpace = $usedspace + AvailableSpace = $AvailableSpace + IsOffline = $result.IsOffline + IsReadOnly = $result.IsReadOnly + IsReadOnlyMedia = $result.IsReadOnlyMedia + IsSparse = $result.IsSparse + NumberOfDiskWrites = $result.NumberOfDiskWrites + NumberOfDiskReads = $result.NumberOfDiskReads + ReadFromDisk = [dbasize]$result.BytesReadFromDisk + WrittenToDisk = [dbasize]$result.BytesWrittenToDisk + VolumeFreeSpace = $VolumeFreeSpace + FileGroupDataSpaceId = $result.FileGroupDataSpaceId + FileGroupType = $result.FileGroupType + FileGroupTypeDescription = $result.FileGroupTypeDescription + FileGroupDefault = $result.FileGroupDefault + FileGroupReadOnly = $result.FileGroupReadOnly + } + } + } + } + } +} +function Get-DbaDatabaseMasterKey { + <# +.SYNOPSIS +Gets specified database master key + +.DESCRIPTION +Gets specified database master key + +.PARAMETER SqlInstance +The target SQL Server instance + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +Get master key from specific database + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate, Database + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDatabaseMasterKey -SqlInstance sql2016 + +Gets all master database keys + +.EXAMPLE +Get-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 + +Gets the master key for the db1 database + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + $masterkey = $db.MasterKey + + if (!$masterkey) { + Write-Message -Message "No master key exists in the $db database on $instance" -Target $db -Level Verbose + continue + } + + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $masterkey -Property ComputerName, InstanceName, SqlInstance, Database, CreateDate, DateLastModified, IsEncryptedByServer + } + } + } +} +function Get-DbaDatabasePartitionFunction { + <# +.SYNOPSIS +Gets database Partition Functions + +.DESCRIPTION +Gets database Partition Functions + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +To get users from specific database(s) + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto populated from the server + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database +Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDatabasePartitionFunction -SqlInstance sql2016 + +Gets all database Partition Functions + +.EXAMPLE +Get-DbaDatabasePartitionFunction -SqlInstance Server1 -Database db1 + +Gets the Partition Functions for the db1 database + +.EXAMPLE +Get-DbaDatabasePartitionFunction -SqlInstance Server1 -ExcludeDatabase db1 + +Gets the Partition Functions for all databases except db1 + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaDatabasePartitionFunction + +Gets the Partition Functions for the databases on Sql1 and Sql2/sqlexpress + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + $partitionfunctions = $db.partitionfunctions + + if (!$partitionfunctions) { + Write-Message -Message "No Partition Functions exist in the $db database on $instance" -Target $db -Level Verbose + continue + } + + $partitionfunctions | foreach { + + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $_ -Property ComputerName, InstanceName, SqlInstance, Database, CreateDate, Name, NumberOfPartitions + } + } + } + } +} +function Get-DbaDatabasePartitionScheme { + <# +.SYNOPSIS +Gets database Partition Schemes + +.DESCRIPTION +Gets database Partition Schemes + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +To get users from specific database(s) + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto populated from the server + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database +Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDatabasePartitionScheme -SqlInstance sql2016 + +Gets all database Partition Schemes + +.EXAMPLE +Get-DbaDatabasePartitionScheme -SqlInstance Server1 -Database db1 + +Gets the Partition Schemes for the db1 database + +.EXAMPLE +Get-DbaDatabasePartitionScheme -SqlInstance Server1 -ExcludeDatabase db1 + +Gets the Partition Schemes for all databases except db1 + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaDatabasePartitionScheme + +Gets the Partition Schemes for the databases on Sql1 and Sql2/sqlexpress + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + $PartitionSchemes = $db.PartitionSchemes + + if (!$PartitionSchemes) { + Write-Message -Message "No Partition Schemes exist in the $db database on $instance" -Target $db -Level Verbose + continue + } + + $PartitionSchemes | foreach { + + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $_ -Property ComputerName, InstanceName, SqlInstance, Database, Name, PartitionFunction + } + } + } + } +} +function Get-DbaDatabaseSpace { + <# + .SYNOPSIS + Returns database file space information for database files on a SQL instance. + + .DESCRIPTION + This function returns database file space information for a SQL Instance or group of SQL Instances. Information is based on a query against sys.database_files and the FILEPROPERTY function to query and return information. + + File free space script borrowed and modified from Glenn Berry's DMV scripts (http://www.sqlskills.com/blogs/glenn/category/dmv-queries/) + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER IncludeSystemDBs + If this switch is enabled, system databases will be processed. By default, only user databases are processed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Space, Storage + Author: Michael Fal (@Mike_Fal), http://mikefal.net + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDatabaseSpace + + .EXAMPLE + Get-DbaDatabaseSpace -SqlInstance localhost + + Returns all user database files and free space information for the localhost. + + .EXAMPLE + Get-DbaDatabaseSpace -SqlInstance localhost | Where-Object {$_.PercentUsed -gt 80} + + Returns all user database files and free space information for the local host. Filters the output object by any files that have a percent used of greater than 80%. + + .EXAMPLE + 'localhost','localhost\namedinstance' | Get-DbaDatabaseSpace + + Returns all user database files and free space information for the localhost and localhost\namedinstance SQL Server instances. Processes data via the pipeline. + + .EXAMPLE + Get-DbaDatabaseSpace -SqlInstance localhost -Database db1, db2 + + Returns database files and free space information for the db1 and db2 on localhost. + #> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystemDBs, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Write-Message -Level System -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")." + + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + DB_NAME() as DBName + ,f.name AS [FileName] + ,fg.name AS [Filegroup] + ,f.physical_name AS [PhysicalName] + ,f.type_desc AS [FileType] + ,CAST(CAST(FILEPROPERTY(f.name, 'SpaceUsed') AS int)/128.0 AS FLOAT) as [UsedSpaceMB] + ,CAST(f.size/128.0 - CAST(FILEPROPERTY(f.name, 'SpaceUsed') AS int)/128.0 AS FLOAT) AS [FreeSpaceMB] + ,CAST((f.size/128.0) AS FLOAT) AS [FileSizeMB] + ,CAST((FILEPROPERTY(f.name, 'SpaceUsed')/(f.size/1.0)) * 100 as FLOAT) as [PercentUsed] + ,CAST((f.growth/128.0) AS FLOAT) AS [GrowthMB] + ,CASE is_percent_growth WHEN 1 THEN 'pct' WHEN 0 THEN 'MB' ELSE 'Unknown' END AS [GrowthType] + ,CASE f.max_size WHEN -1 THEN 2147483648. ELSE CAST((f.max_size/128.0) AS FLOAT) END AS [MaxSizeMB] + ,CAST((f.size/128.0) AS FLOAT) - CAST(CAST(FILEPROPERTY(f.name, 'SpaceUsed') AS int)/128.0 AS FLOAT) AS [SpaceBeforeAutoGrow] + ,CASE f.max_size WHEN (-1) + THEN CAST(((2147483648.) - CAST(FILEPROPERTY(f.name, 'SpaceUsed') AS int))/128.0 AS FLOAT) + ELSE CAST((f.max_size - CAST(FILEPROPERTY(f.name, 'SpaceUsed') AS int))/128.0 AS FLOAT) + END AS [SpaceBeforeMax] + ,CASE f.growth WHEN 0 THEN 0.00 + ELSE CASE f.is_percent_growth WHEN 0 + THEN CASE f.max_size + WHEN (-1) + THEN CAST(((((2147483648.)-f.Size)/f.Growth)*f.Growth)/128.0 AS FLOAT) + ELSE CAST((((f.max_size-f.Size)/f.Growth)*f.Growth)/128.0 AS FLOAT) + END + WHEN 1 + THEN CASE f.max_size + WHEN (-1) + THEN CAST(CONVERT([int],f.Size*power((1)+CONVERT([float],f.Growth)/(100),CONVERT([int],log10(CONVERT([float],(2147483648.))/CONVERT([float],f.Size))/log10((1)+CONVERT([float],f.Growth)/(100)))))/128.0 AS FLOAT) + ELSE CAST(CONVERT([int],f.Size*power((1)+CONVERT([float],f.Growth)/(100),CONVERT([int],log10(CONVERT([float],f.Max_Size)/CONVERT([float],f.Size))/log10((1)+CONVERT([float],f.Growth)/(100)))))/128.0 AS FLOAT) + END + ELSE (0) + END + END AS [PossibleAutoGrowthMB] + , CASE f.max_size WHEN -1 THEN 0 + ELSE CASE f.growth + WHEN 0 THEN (f.max_size - f.size)/128 + ELSE CASE f.is_percent_growth + WHEN 0 + THEN CAST((f.max_size - f.size - ( CONVERT(FLOAT,FLOOR((f.max_size-f.Size)/f.Growth)*f.Growth)))/128.0 AS FLOAT) + ELSE CAST((f.max_size - f.size - ( CONVERT([int],f.Size*power((1)+CONVERT([float],f.Growth)/(100),CONVERT([int],log10(CONVERT([float],f.Max_Size)/CONVERT([float],f.Size))/log10((1)+CONVERT([float],f.Growth)/(100)))))))/128.0 AS FLOAT) + END + END + END AS [UnusableSpaceMB] + + FROM sys.database_files AS f WITH (NOLOCK) + LEFT OUTER JOIN sys.filegroups AS fg WITH (NOLOCK) + ON f.data_space_id = fg.data_space_id" + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance." -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failed to process Instance $Instance." -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -lt 9) { + Write-Message -Level Warning -Message "SQL Server 2000 not supported. $server skipped." + continue + } + + #If IncludeSystemDBs is true, include systemdbs + #look at all databases, online/offline/accessible/inaccessible and tell user if a db can't be queried. + try { + if (Test-Bound "Database") { + $dbs = $server.Databases | Where-Object Name -In $Database + } + elseif ($IncludeSystemDBs) { + $dbs = $server.Databases | Where-Object IsAccessible + } + else { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.IsSystemObject -eq 0 } + } + + if (Test-Bound "ExcludeDatabase") { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + } + catch { + Stop-Function -Message "Unable to gather databases for $instance." -ErrorRecord $_ -Continue + } + + foreach ($db in $dbs) { + try { + Write-Message -Level Verbose -Message "Querying $instance - $db." + If ($db.status -ne 'Normal' -or $db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "$db is not accessible." -Target $db + continue + } + #Execute query against individual database and add to output + foreach ($row in ($db.ExecuteWithResults($sql)).Tables.Rows) { + if ($row.UsedSpaceMB -is [System.DBNull]) { + $UsedMB = 0 + } + else { + $UsedMB = [Math]::Round($row.UsedSpaceMB) + } + if ($row.FreeSpaceMB -is [System.DBNull]) { + $FreeMB = 0 + } + else { + $FreeMB = [Math]::Round($row.FreeSpaceMB) + } + if ($row.PercentUsed -is [System.DBNull]) { + $PercentUsed = 0 + } + else { + $PercentUsed = [Math]::Round($row.PercentUsed) + } + if ($row.SpaceBeforeMax -is [System.DBNull]) { + $SpaceUntilMax = 0 + } + else { + $SpaceUntilMax = [Math]::Round($row.SpaceBeforeMax) + } + if ($row.UnusableSpaceMB -is [System.DBNull]) { + $UnusableSpace = 0 + } + else { + $UnusableSpace = [Math]::Round($row.UnusableSpaceMB) + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DBName + FileName = $row.FileName + FileGroup = $row.FileGroup + PhysicalName = $row.PhysicalName + FileType = $row.FileType + UsedSpaceMB = $UsedMB + FreeSpaceMB = $FreeMB + FileSizeMB = $row.FileSizeMB + PercentUsed = $PercentUsed + AutoGrowth = $row.GrowthMB + AutoGrowType = $row.GrowthType + SpaceUntilMaxSizeMB = $SpaceUntilMax + AutoGrowthPossibleMB = $row.PossibleAutoGrowthMB + UnusableSpaceMB = $UnusableSpace + } + } + } + catch { + Stop-Function -Message "Unable to query $instance - $db." -Target $db -ErrorRecord $_ -Continue + } + } + } + } + + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaDatabaseFreeSpace + } +} + +#ValidationTags#Messaging,FlowControl,Pipeline# +function Get-DbaDatabaseState { + <# +.SYNOPSIS +Gets various options for databases, hereby called "states" + +.DESCRIPTION +Gets some common "states" on databases: + - "RW" options : READ_ONLY or READ_WRITE + - "Status" options : ONLINE, OFFLINE, EMERGENCY, RESTORING + - "Access" options : SINGLE_USER, RESTRICTED_USER, MULTI_USER + +Returns an object with SqlInstance, Database, RW, Status, Access + +.PARAMETER SqlInstance +The SQL Server that you're connecting to + +.PARAMETER SqlCredential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database +Author: niphlod + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaDatabaseState + +.EXAMPLE +Get-DbaDatabaseState -SqlInstance sqlserver2014a + +Gets options for all databases of the sqlserver2014a instance + +.EXAMPLE +Get-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR, Accounting + +Gets options for both HR and Accounting database of the sqlserver2014a instance + +.EXAMPLE +Get-DbaDatabaseState -SqlInstance sqlserver2014a -Exclude HR + +Gets options for all databases of the sqlserver2014a instance except HR + +.EXAMPLE +'sqlserver2014a', 'sqlserver2014b' | Get-DbaDatabaseState + +Gets options for all databases of sqlserver2014a and sqlserver2014b instances + +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseLiteralInitializerForHashtable", "")] + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $DbStatesQuery = @' +SELECT +Name = name, +Access = user_access_desc, +Status = state_desc, +RW = CASE WHEN is_read_only = 0 THEN 'READ_WRITE' ELSE 'READ_ONLY' END +FROM sys.databases +'@ + + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $dbStates = $server.Query($DbStatesQuery) + $dbs = $dbStates | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + # "normal" hashtable doesn't account for case sensitivity + $dbStatesHash = New-Object -TypeName System.Collections.Hashtable + foreach ($db in $dbStates) { + $dbStatesHash.Add($db.Name, [pscustomobject]@{ + Access = $db.Access + Status = $db.Status + RW = $db.RW + }) + } + foreach ($db in $dbs) { + $db_status = $dbStatesHash[$db.Name] + [PSCustomObject]@{ + SqlInstance = $server.Name + InstanceName = $server.ServiceName + ComputerName = $server.NetName + DatabaseName = $db.Name + RW = $db_status.RW + Status = $db_status.Status + Access = $db_status.Access + Database = $server.Databases[$db.Name] + } | Select-DefaultView -ExcludeProperty Database + } + } + } +} + +function Get-DbaDatabaseUdf { + <# +.SYNOPSIS +Gets database User Defined Functions + +.DESCRIPTION +Gets database User Defined Functions + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +To get User Defined Functions from specific database(s) + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto populated from the server + +.PARAMETER ExcludeSystemUdf +This switch removes all system objects from the UDF collection + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Security, Database +Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDatabaseUdf -SqlInstance sql2016 + +Gets all database User Defined Functions + +.EXAMPLE +Get-DbaDatabaseUdf -SqlInstance Server1 -Database db1 + +Gets the User Defined Functions for the db1 database + +.EXAMPLE +Get-DbaDatabaseUdf -SqlInstance Server1 -ExcludeDatabase db1 + +Gets the User Defined Functions for all databases except db1 + +.EXAMPLE +Get-DbaDatabaseUdf -SqlInstance Server1 -ExcludeSystemUdf + +Gets the User Defined Functions for all databases that are not system objects (there can be 100+ system User Defined Functions in each DB) + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaDatabaseUdf + +Gets the User Defined Functions for the databases on Sql1 and Sql2/sqlexpress + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemUdf, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + + $UserDefinedFunctions = $db.UserDefinedFunctions + + if (!$UserDefinedFunctions) { + Write-Message -Message "No User Defined Functions exist in the $db database on $instance" -Target $db -Level Verbose + continue + } + if (Test-Bound -ParameterName ExcludeSystemUdf) { + $UserDefinedFunctions = $UserDefinedFunctions | Where-Object { $_.IsSystemObject -eq $false } + } + + $UserDefinedFunctions | foreach { + + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $_ -Property ComputerName, InstanceName, SqlInstance, Database, Schema, CreateDate, DateLastModified, Name, DataType + } + } + } + } +} +function Get-DbaDatabaseUser { + <# +.SYNOPSIS +Gets database users + +.DESCRIPTION +Gets database users + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +To get users from specific database(s) + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto populated from the server + +.PARAMETER ExcludeSystemUser +This switch removes all system objects from the user collection + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Security, Database +Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDatabaseUser -SqlInstance sql2016 + +Gets all database users + +.EXAMPLE +Get-DbaDatabaseUser -SqlInstance Server1 -Database db1 + +Gets the users for the db1 database + +.EXAMPLE +Get-DbaDatabaseUser -SqlInstance Server1 -ExcludeDatabase db1 + +Gets the users for all databases except db1 + +.EXAMPLE +Get-DbaDatabaseUser -SqlInstance Server1 -ExcludeSystemUser + +Gets the users for all databases that are not system objects, like 'dbo', 'guest' or 'INFORMATION_SCHEMA' + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaDatabaseUser + +Gets the users for the databases on Sql1 and Sql2/sqlexpress + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemUser, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + + $users = $db.users + + if (!$users) { + Write-Message -Message "No users exist in the $db database on $instance" -Target $db -Level Verbose + continue + } + if (Test-Bound -ParameterName ExcludeSystemUser) { + $users = $users | Where-Object { $_.IsSystemObject -eq $false } + } + + $users | foreach { + + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $_ -Property ComputerName, InstanceName, SqlInstance, Database, CreateDate, DateLastModified, Name, Login, LoginType, AuthenticationType, State, HasDbAccess, DefaultSchema + } + } + } + } +} +function Get-DbaDatabaseView { + <# + .SYNOPSIS + Gets database views for each SqlInstance. + + .DESCRIPTION + Gets database views for each SqlInstance. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + To get views from specific database(s) - this list is auto populated from the server. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server. + + .PARAMETER ExcludeSystemView + This switch removes all system objects from the view collection. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Database + Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDatabaseView -SqlInstance sql2016 + + Gets all database views + + .EXAMPLE + Get-DbaDatabaseView -SqlInstance Server1 -Database db1 + + Gets the views for the db1 database + + .EXAMPLE + Get-DbaDatabaseView -SqlInstance Server1 -ExcludeDatabase db1 + + Gets the views for all databases except db1 + + .EXAMPLE + Get-DbaDatabaseView -SqlInstance Server1 -ExcludeSystemView + + Gets the views for all databases that are not system objects (there can be 400+ system views in each DB) + + .EXAMPLE + 'Sql1','Sql2/sqlexpress' | Get-DbaDatabaseView + + Gets the views for the databases on Sql1 and Sql2/sqlexpress + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemView, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + $views = $db.views + + if (!$views) { + Write-Message -Message "No views exist in the $db database on $instance" -Target $db -Level Verbose + continue + } + if (Test-Bound -ParameterName ExcludeSystemView) { + $views = $views | Where-Object { $_.IsSystemObject -eq $false } + } + + $views | Foreach-Object { + + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $_ -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $_ -Property ComputerName, InstanceName, SqlInstance, Database, Schema, CreateDate, DateLastModified, Name + } + } + } + } +} +function Get-DbaDbCertificate { + <# +.SYNOPSIS +Gets database certificates + +.DESCRIPTION +Gets database certificates + +.PARAMETER SqlInstance +The target SQL Server instance + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Database +Get certificate from specific database + +.PARAMETER ExcludeDatabase +Database(s) to ignore when retrieving certificates. + +.PARAMETER Certificate +Get specific certificate + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaDbCertificate -SqlInstance sql2016 + +Gets all certificates + +.EXAMPLE +Get-DbaDbCertificate -SqlInstance Server1 -Database db1 + +Gets the certificate for the db1 database + +.EXAMPLE +Get-DbaDbCertificate -SqlInstance Server1 -Database db1 -Certificate cert1 + +Gets the cert1 certificate within the db1 database + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Certificate, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaDatabaseCertificate + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = Get-DbaDatabase -SqlInstance $server | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "$db is not accessible, skipping" + continue + } + $dbName = $db.Name + $currentdb = $server.Databases[$dbName] + + if ($null -eq $currentdb) { + Write-Message -Message "Database '$db' does not exist on $instance" -Target $currentdb -Level Verbose + continue + } + + if ($null -eq $currentdb.Certificates) { + Write-Message -Message "No certificate exists in the $db database on $instance" -Target $currentdb -Level Verbose + continue + } + + $certs = $currentdb.Certificates + if ($Certificate) { + $certs = $certs | Where-Object Name -in $Certificate + } + + foreach ($cert in $certs) { + + Add-Member -Force -InputObject $cert -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $cert -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $cert -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $cert -MemberType NoteProperty -Name Database -value $currentdb.Name + + Select-DefaultView -InputObject $cert -Property ComputerName, InstanceName, SqlInstance, Database, Name, Subject, StartDate, ActiveForServiceBrokerDialog, ExpirationDate, Issuer, LastBackupDate, Owner, PrivateKeyEncryptionType, Serial + } + } + } + } +} +function Get-DbaDbCheckConstraint { + <# + .SYNOPSIS + Gets database Check constraints. + + .DESCRIPTION + Gets database Checks constraints. + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials + + .PARAMETER Database + To get Checks from specific database(s) + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server + + .PARAMETER ExcludeSystemTable + This switch removes all system objects from the table collection + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Author: Cláudio Silva ( @ClaudioESSilva | https://claudioessilva.eu) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDbCheckConstraint -SqlInstance sql2016 + + Gets all database check constraints. + + .EXAMPLE + Get-DbaDbCheckConstraint -SqlInstance Server1 -Database db1 + + Gets the check constraints for the db1 database. + + .EXAMPLE + Get-DbaDbCheckConstraint -SqlInstance Server1 -ExcludeDatabase db1 + + Gets the check constraints for all databases except db1. + + .EXAMPLE + Get-DbaDbCheckConstraint -SqlInstance Server1 -ExcludeSystemTable + + Gets the check constraints for all databases that are not system objects. + + .EXAMPLE + 'Sql1','Sql2/sqlexpress' | Get-DbaDbCheckConstraint + + Gets the check constraints for the databases on Sql1 and Sql2/sqlexpress. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemTable, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + foreach($tbl in $db.Tables) { + if ( (Test-Bound -ParameterName ExcludeSystemTable) -and $tbl.IsSystemObject ) { + continue + } + + if ($tbl.Checks.Count -eq 0) { + Write-Message -Message "No Checks exist in $tbl table on the $db database on $instance" -Target $tbl -Level Verbose + continue + } + + foreach ($ck in $tbl.Checks) { + Add-Member -Force -InputObject $ck -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $ck -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $ck -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $ck -MemberType NoteProperty -Name Database -value $db.Name + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Parent', 'ID', 'CreateDate', + 'DateLastModified', 'Name', 'IsEnabled', 'IsChecked', 'NotForReplication', 'Text', 'State' + Select-DefaultView -InputObject $ck -Property $defaults + } + } + } + } + } +} +function Get-DbaDbCompression { + <# + .SYNOPSIS + Gets tables and indexes size and current compression settings. + + .DESCRIPTION + This function gets the current size and compression for all objects in the specified database(s), if no database is specified it will return all objects in all user databases. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Jess Pomfret (@jpomfret jesspomfret.com) + Tags: Compression, Table, Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDbCompression -SqlInstance localhost + + Returns objects size and current compression level for all user databases. + + .EXAMPLE + Get-DbaDbCompression -SqlInstance localhost -Database TestDatabase + + Returns objects size and current compression level for objects within the TestDatabase database. + + .EXAMPLE + Get-DbaDbCompression -SqlInstance localhost -ExcludeDatabase TestDatabases + + Returns objects size and current compression level for objects in all databases except the TestDatabase database. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + + try { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.IsSystemObject -eq 0 } + + if ($Database) { + $dbs = $dbs | Where-Object { $_.Name -In $Database } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $_.Name -NotIn $ExcludeDatabase } + } + } + catch { + Stop-Function -Message "Unable to gather list of databases for $instance" -Target $instance -ErrorRecord $_ -Continue + } + + foreach ($db in $dbs) { + try { + foreach ($obj in $server.Databases[$($db.name)].Tables) { + if ($obj.HasHeapIndex) { + foreach ($p in $obj.PhysicalPartitions) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $null + Partition = $p.PartitionNumber + IndexID = 0 + IndexType = "Heap" + DataCompression = $p.DataCompression + SizeCurrent = [dbasize]($obj.DataSpaceUsed * 1024) + RowCount = $obj.RowCount + } + } + } + + foreach ($index in $obj.Indexes) { + foreach ($p in $index.PhysicalPartitions) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $index.Name + Partition = $p.PartitionNumber + IndexID = $index.ID + IndexType = $index.IndexType + DataCompression = $p.DataCompression + SizeCurrent = if($index.IndexType -eq "ClusteredIndex") { [dbasize]($obj.DataSpaceUsed * 1024) } else { [dbasize]($index.SpaceUsed * 1024) } + RowCount = $p.RowCount + } + } + } + + } + } + catch { + Stop-Function -Message "Unable to query $instance - $db" -Target $db -ErrorRecord $_ -Continue + } + + } + } + } +} +function Get-DbaDbExtentDiff { + <# + .SYNOPSIS + What percentage of a database has changed since the last full backup + + .DESCRIPTION + This is only an implementation of the script created by Paul S. Randal to find what percentage of a database has changed since the last full backup. + https://www.sqlskills.com/blogs/paul/new-script-how-much-of-the-database-has-changed-since-the-last-full-backup/ + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup, Database + Author: Viorel Ciucu, viorel.ciucu@gmail.com, cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: GNU GPL v3 https://opensource.org/licenses/GPL-3.0 + + .LINK + http://dbatools.io/Get-DbaDbExtentDiff + + .EXAMPLE + Get the changes for the DBA database. + Get-DbaDbExtentDiff -SqlInstance SQL2016 -Database DBA + + .EXAMPLE + Get the changes for the DB01 database on multiple servers. + Get-DbaDbExtentDiff -SqlInstance $SQL2017N1, $SQL2017N2, $SQL2016 -Database DB01 -SqlCredential $Cred + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlServer')] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$EnableException + ) + + begin { + $rex = [regex]':(?[\d]+)\)' + function Get-DbaExtent ([string[]]$field) { + $res = 0 + foreach ($f in $field) { + $extents = $rex.Matches($f) + if ($extents.Count -eq 1) { + $res += 1 + } + else { + $pages = [int]$extents[1].Groups['extent'].Value - [int]$extents[0].Groups['extent'].Value + $res += $pages / 8 + 1 + } + } + return $res + } + } + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-DbaInstance -SqlInstance $instance -SqlCredential $SqlCredential -NonPooled + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + $sourcedbs = @() + foreach ($db in $dbs) { + if ($db.IsAccessible -ne $true) { + Write-Message -Level Verbose -Message "$db is not accessible on $instance, skipping" + } + else { + $sourcedbs += $db + } + } + + if ($server.VersionMajor -ge 14 ) { + foreach ($db in $sourcedbs) { + $DBCCPageQueryDMV = " + SELECT + SUM(total_page_count) / 8 as [ExtentsTotal], + SUM(modified_extent_page_count) / 8 as [ExtentsChanged], + 100.0 * SUM(modified_extent_page_count)/SUM(total_page_count) as [ChangedPerc] + FROM sys.dm_db_file_space_usage + " + $DBCCPageResults = $server.Query($DBCCPageQueryDMV, $db.Name) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + ExtentsTotal = $DBCCPageResults.ExtentsTotal + ExtentsChanged = $DBCCPageResults.ExtentsChanged + ChangedPerc = [math]::Round($DBCCPageResults.ChangedPerc, 2) + } + } + } + else { + $MasterFilesQuery = " + SELECT [file_id], [size], database_id, db_name(database_id) as dbname FROM master.sys.master_files + WHERE [type_desc] = N'ROWS' + " + $MasterFiles = $server.Query($MasterFilesQuery) + $MasterFiles = $MasterFiles | Where-Object dbname -In $sourcedbs.Name + $MasterFilesGrouped = $MasterFiles | Group-Object -Property dbname + + foreach ($db in $MasterFilesGrouped) { + $sizeTotal = 0 + $dbExtents = @() + foreach ($results in $db.Group) { + $extentID = 0 + $sizeTotal = $sizeTotal + $results.size / 8 + while ($extentID -lt $results.size) { + $pageID = $extentID + 6 + $DBCCPageQuery = "DBCC PAGE ('$($results.dbname)', $($results.file_id), $pageID, 3) WITH TABLERESULTS, NO_INFOMSGS" + $DBCCPageResults = $server.Query($DBCCPageQuery) + $dbExtents += $DBCCPageResults | Where-Object { $_.VALUE -eq ' CHANGED' -And $_.ParentObject -like 'DIFF_MAP*'} + $extentID = $extentID + 511232 + } + } + $extents = Get-DbaExtent $dbExtents.Field + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + ExtentsTotal = $sizeTotal + ExtentsChanged = $extents + ChangedPerc = [math]::Round(($extents / $sizeTotal * 100), 2) + } + } + } + } + } +} +function Get-DbaDbForeignKey { + <# + .SYNOPSIS + Gets database Foreign Keys. + + .DESCRIPTION + Gets database Foreign Keys. + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials + + .PARAMETER Database + To get Foreign Keys from specific database(s) + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server + + .PARAMETER ExcludeSystemTable + This switch removes all system objects from the tables collection + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database,ForeignKey, Table + Author: Cláudio Silva ( @ClaudioESSilva | https://claudioessilva.eu) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDbForeignKey -SqlInstance sql2016 + + Gets all database Foreign Keys. + + .EXAMPLE + Get-DbaDbForeignKey -SqlInstance Server1 -Database db1 + + Gets the Foreign Keys for the db1 database. + + .EXAMPLE + Get-DbaDbForeignKey -SqlInstance Server1 -ExcludeDatabase db1 + + Gets the Foreign Keys for all databases except db1. + + .EXAMPLE + Get-DbaDbForeignKey -SqlInstance Server1 -ExcludeSystemTable + + Gets the Foreign Keys from all tables that are not system objects from all databases. + + .EXAMPLE + 'Sql1','Sql2/sqlexpress' | Get-DbaDbForeignKey + + Gets the Foreign Keys for the databases on Sql1 and Sql2/sqlexpress. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemTable, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + + foreach($tbl in $db.Tables) { + if ( (Test-Bound -ParameterName ExcludeSystemTable) -and $tbl.IsSystemObject ) { + continue + } + + if ($tbl.ForeignKeys.Count -eq 0) { + Write-Message -Message "No Foreign Keys exist in $tbl table on the $db database on $instance" -Target $tbl -Level Verbose + continue + } + + foreach ($fk in $tbl.ForeignKeys) { + Add-Member -Force -InputObject $fk -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $fk -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $fk -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $fk -MemberType NoteProperty -Name Database -value $db.Name + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Table', 'ID', 'CreateDate', + 'DateLastModified', 'Name', 'IsEnabled', 'IsChecked', 'NotForReplication', 'ReferencedKey', 'ReferencedTable', 'ReferencedTableSchema' + Select-DefaultView -InputObject $fk -Property $defaults + } + } + } + } + } +} +function Get-DbaDbMailHistory { + <# + .SYNOPSIS + Gets the history of mail sent from a SQL instance + + .DESCRIPTION + Gets the history of mail sent from a SQL instance + + .PARAMETER SqlInstance + The SQL Server instance, or instances. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER Since + Datetime object used to narrow the results to the send request date + + .PARAMETER Status + Narrow the results by status. Valid values include Unsent, Sent, Failed and Retrying + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Logging + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDbMailHistory + + .EXAMPLE + Get-DbaDbMailHistory -SqlInstance sql01\sharepoint + + Returns the entire dbmail history on sql01\sharepoint + + .EXAMPLE + Get-DbaDbMailHistory -SqlInstance sql01\sharepoint | Select * + + Returns the entire dbmail history on sql01\sharepoint then return a bunch more columns + + .EXAMPLE + $servers = "sql2014","sql2016", "sqlcluster\sharepoint" + $servers | Get-DbaDbMailHistory + + Returns the all dbmail history for "sql2014","sql2016" and "sqlcluster\sharepoint" + +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [DateTime]$Since, + [ValidateSet('Unsent', 'Sent', 'Failed', 'Retrying')] + [string]$Status, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category Connectiondbmail -dbmailRecord $_ -Target $instance -Continue + } + + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + mailitem_id as MailItemId, + a.profile_id as ProfileId, + p.name as Profile, + recipients as Recipients, + copy_recipients as CopyRecipients, + blind_copy_recipients as BlindCopyRecipients, + subject as Subject, + body as Body, + body_format as BodyFormat, + importance as Importance, + sensitivity as Sensitivity, + file_attachments as FileAttachments, + attachment_encoding as AttachmentEncoding, + query as Query, + execute_query_database as ExecuteQueryDatabase, + attach_query_result_as_file as AttachQueryResultAsFile, + query_result_header as QueryResultHeader, + query_result_width as QueryResultWidth, + query_result_separator as QueryResultSeparator, + exclude_query_output as ExcludeQueryOutput, + append_query_error as AppendQueryError, + send_request_date as SendRequestDate, + send_request_user as SendRequestUser, + sent_account_id as SentAccountId, + CASE sent_status + WHEN 'unsent' THEN 'Unsent' + WHEN 'sent' THEN 'Sent' + WHEN 'failed' THEN 'Failed' + WHEN 'retrying' THEN 'Retrying' + END AS SentStatus, + sent_date as SentDate, + last_mod_date as LastModDate, + a.last_mod_user as LastModUser + from msdb.dbo.sysmail_allitems a + join msdb.dbo.sysmail_profile p + on a.profile_id = p.profile_id" + + if ($Since -or $Status) { + $wherearray = @() + + if ($Since) { + $wherearray += "send_request_date >= '$($Since.ToString("yyyy-MM-ddTHH:mm:ss"))'" + } + + if ($Status) { + $Status = $Status -join "', '" + $wherearray += "sent_status in ('$Status')" + } + + $wherearray = $wherearray -join ' and ' + $where = "where $wherearray" + $sql = "$sql $where" + } + + Write-Message -Level Debug -Message $sql + + try { + $server.Query($sql) | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Profile, Recipients, CopyRecipients, BlindCopyRecipients, Subject, Importance, Sensitivity, FileAttachments, AttachmentEncoding, SendRequestDate, SendRequestUser, SentStatus, SentDate + } + catch { + Stop-Function -Message "Query failure" -ErrorRecord $_ -Continue + } + } + } +} +function Get-DbaDbMailLog { + <# + .SYNOPSIS + Gets the DBMail log from a SQL instance + + .DESCRIPTION + Gets the DBMail log from a SQL instance + + .PARAMETER SqlInstance + The SQL Server instance, or instances. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER Since + Datetime object used to narrow the results to the send request date + + .PARAMETER Type + Narrow the results by type. Valid values include Error, Warning, Success, Information, Internal + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Logging + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDbMailLog + + .EXAMPLE + Get-DbaDbMailLog -SqlInstance sql01\sharepoint + + Returns the entire dbmail log on sql01\sharepoint + + .EXAMPLE + Get-DbaDbMailLog -SqlInstance sql01\sharepoint | Select * + + Returns the entire dbmail log on sql01\sharepoint then return a bunch more columns + + .EXAMPLE + $servers = "sql2014","sql2016", "sqlcluster\sharepoint" + $servers | Get-DbaDbMailLog -Type Error, Information + + Returns only the Error and Information dbmail log for "sql2014","sql2016" and "sqlcluster\sharepoint" + +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [DateTime]$Since, + [ValidateSet('Error', 'Warning', 'Success', 'Information', 'Internal')] + [string[]]$Type, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category Connectiondbmail -dbmailRecord $_ -Target $instance -Continue + } + + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + log_id as LogId, + CASE event_type + WHEN 'error' THEN 'Error' + WHEN 'warning' THEN 'Warning' + WHEN 'information' THEN 'Information' + WHEN 'success' THEN 'Success' + WHEN 'internal' THEN 'Internal' + ELSE event_type + END as EventType, + log_date as LogDate, + REPLACE(description, CHAR(10)+')', '') as Description, + process_id as ProcessId, + mailitem_id as MailItemId, + account_id as AccountId, + last_mod_date as LastModDate, + last_mod_user as LastModUser, + last_mod_user as [Login] + FROM msdb.dbo.sysmail_event_log" + + if ($Since -or $Type) { + $wherearray = @() + + if ($Since) { + $wherearray += "log_date >= '$($Since.ToString("yyyy-MM-ddTHH:mm:ss"))'" + } + + if ($Type) { + $combinedtype = $Type -join "', '" + $wherearray += "event_type in ('$combinedtype')" + } + + $wherearray = $wherearray -join ' and ' + $where = "where $wherearray" + $sql = "$sql $where" + } + + Write-Message -Level Debug -Message $sql + + try { + $server.Query($sql) | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, LogDate, EventType, Description, Login + } + catch { + Stop-Function -Message "Query failure" -InnerErrorRecord $_ -Continue + } + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaDbPageInfo { + <# + .SYNOPSIS + Get-DbaDbPageInfo will return page information for a database + + .DESCRIPTION + Get-DbaDbPageInfo is able to return information about the pages in a database. + It's possible to return the information for multiple databases and filter on specific databases, schemas and tables. + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Filter to only get specific databases + + .PARAMETER Schema + Filter to only get specific schemas + + .PARAMETER Table + Filter to only get specific tables + + .PARAMETER InputObject + Enables piping from Get-DbaDatabase + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Page + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDbPageInfo + + .EXAMPLE + Get-DbaDbPageInfo -SqlInstance sql2017 + + Returns page information for all databases on sql2017 + + .EXAMPLE + Get-DbaDbPageInfo -SqlInstance sql2017, sql2016 -Database testdb + + Returns page information for the testdb on sql2017 and sql2016 + + .EXAMPLE + $servers | Get-DbaDatabase -Database testdb | Get-DbaDbPageInfo + + Returns page information for the testdb on all $servers + #> + [CmdLetBinding()] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Database, + [string[]]$Schema, + [string[]]$Table, + [parameter(ValueFromPipeline)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [switch]$EnableException + ) + begin { + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, [Database] = DB_NAME(DB_ID()), + ss.name AS [Schema], st.name AS [Table], dbpa.page_type_desc AS PageType, + dbpa.page_free_space_percent AS PageFreePercent, + IsAllocated = + CASE dbpa.is_allocated + WHEN 0 THEN 'False' + WHEN 1 THEN 'True' + END, + IsMixedPage = + CASE dbpa.is_mixed_page_allocation + WHEN 0 THEN 'False' + WHEN 1 THEN 'True' + END + FROM sys.dm_db_database_page_allocations(DB_ID(), NULL, NULL, NULL, 'DETAILED') AS dbpa + INNER JOIN sys.tables AS st ON st.object_id = dbpa.object_id + INNER JOIN sys.schemas AS ss ON ss.schema_id = st.schema_id" + + if ($Schema) { + $sql = "$sql WHERE ss.name IN ('$($Schema -join "','")')" + } + + if ($Table) { + if ($schema) { + $sql = "$sql AND st.name IN ('$($Table -join "','")')" + } + else { + $sql = "$sql WHERE st.name IN ('$($Table -join "','")')" + } + } + } + process { + # Loop through all the instances + foreach ($instance in $SqlInstance) { + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $InputObject += $server.Databases | Where-Object { $_.Name -in $Database } + } + else { + $InputObject += $server.Databases + } + } + + # Loop through each of databases + foreach ($db in $InputObject) { + # Revalidate the version of the server in case db is piped in + try { + if ($db.Parent.VersionMajor -ge 11) { + $db.Query($sql) + } + else + { + Stop-Function -Message "Unsupported SQL Server version" -Target $db -Continue + } + } + catch { + Stop-Function -Message "Something went wrong executing the query" -ErrorRecord $_ -Target $instance -Continue + } + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaDbQueryStoreOptions { + <# + .SYNOPSIS + Get the Query Store configuration for Query Store enabled databases. + + .DESCRIPTION + Retrieves and returns the Query Store configuration for every database that has the Query Store feature enabled. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.QueryStoreOptions + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: QueryStore + Author: Enrico van de Laar ( @evdlaar ) + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaQueryStoreOptions + + .EXAMPLE + Get-DbaDbQueryStoreOptions -SqlInstance ServerA\sql + + Returns Query Store configuration settings for every database on the ServerA\sql instance. + + .EXAMPLE + Get-DbaDbQueryStoreOptions -SqlInstance ServerA\sql | Where-Object {$_.ActualState -eq "ReadWrite"} + + Returns the Query Store configuration for all databases on ServerA\sql where the Query Store feature is in Read/Write mode. + + .EXAMPLE + Get-DbaDbQueryStoreOptions -SqlInstance localhost | format-table -AutoSize -Wrap + + Returns Query Store configuration settings for every database on the ServerA\sql instance inside a table format. + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $ExcludeDatabase += 'master', 'tempdb' + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 13 + } + catch { + Write-Message -Level Warning -Message "Can't connect to $instance. Moving on." + continue + } + + # We have to exclude all the system databases since they cannot have the Query Store feature enabled + $dbs = Get-DbaDatabase -SqlInstance $server -ExcludeDatabase $ExcludeDatabase -Database $Database | Where-Object IsAccessible + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $($db.Name) on $instance" + $QSO = $db.QueryStoreOptions + + Add-Member -Force -InputObject $QSO -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $QSO -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $QSO -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $QSO -MemberType NoteProperty Database -value $db.Name + Select-DefaultView -InputObject $QSO -Property ComputerName, InstanceName, SqlInstance, Database, ActualState, DataFlushIntervalInSeconds, StatisticsCollectionIntervalInMinutes, MaxStorageSizeInMB, CurrentStorageSizeInMB, QueryCaptureMode, SizeBasedCleanupMode, StaleQueryThresholdInDays + } + } + } +} +function Get-DbaDbRecoveryModel { + <# + .SYNOPSIS + Get-DbaDbRecoveryModel displays the Recovery Model. + + .DESCRIPTION + Get-DbaDbRecoveryModel displays the Recovery Model for all databases. This is the default, you can filter using -Database, -ExcludeDatabase, -RecoveryModel + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. if unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER RecoveryModel + Filters the output based on Recovery Model. Valid options are Simple, Full and BulkLogged + + Details about the recovery models can be found here: + https://docs.microsoft.com/en-us/sql/relational-databases/backup-restore/recovery-models-sql-server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Recovery, RecoveryModel, Simple, Full, Bulk, BulkLogged + Author: Viorel Ciucu (@viorelciucu), https://www.cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDbRecoveryModel + + .EXAMPLE + Get-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel BulkLogged -Verbose + + Gets all databases on SQL Server instance sql2014 having RecoveryModel set to BulkLogged + + .EXAMPLE + Get-DbaDbRecoveryModel -SqlInstance sql2014 -Database TestDB + + Gets recovery model information for TestDB. If TestDB does not exist on the instance we don't return anythig. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [string[]]$RecoveryModel, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$EnableException + ) + begin { + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name', 'Status', 'IsAccessible', 'RecoveryModel', + 'LastBackupDate as LastFullBackup', 'LastDifferentialBackupDate as LastDiffBackup', + 'LastLogBackupDate as LastLogBackup' + } + process { + $params = @{ + SqlInstance = $SqlInstance + SqlCredential = $SqlCredential + Database = $Database + ExcludeDatabase = $ExcludeDatabase + EnableException = $EnableException + } + + if ($RecoveryModel) { + Get-DbaDatabase @params | Where-Object RecoveryModel -in $RecoveryModel | Where-Object IsAccessible | Select-DefaultView -Property $defaults + } + else { + Get-DbaDatabase @params | Select-DefaultView -Property $defaults + } + } +} +function Get-DbaDbRole { + <# +.SYNOPSIS +Get database roles on a Sql instance. + +.DESCRIPTION +Get database roles on a Sql instance. + +Default output includes columns SQLServer, Database, Role. + +.PARAMETER SQLInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER ExcludeFixedRole +Excludes all fixed roles. + +.PARAMETER Credential +Credential object used to connect to the SQL Server as a different user. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Roles, Database, Security +Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK + https://dbatools.io/Get-DbaDbRole + +.EXAMPLE +Get-DbaDbRole -SqlInstance ServerA + +Returns a custom object displaying SQLServer, Database, Role for all DatabaseRoles on sql instance ServerA. + +.EXAMPLE +Get-DbaDbRole -SqlInstance ServerA | Out-Gridview + +Returns a gridview displaying SQLServer, Database, Role for all DatabaseRoles on sql instance ServerA. + +.EXAMPLE +Get-DbaDbRole -SqlInstance ServerB\sql16 -ExcludeDatabase DBADB,TestDB + +Returns SQLServer, Database, Role for DatabaseRoles on sql instance ServerB\sql16, except those in databases DBADB and TestDB. + +.EXAMPLE +'ServerB\sql16','ServerA' | Get-DbaDbRole + +Returns SQLServer, Database, Role for DatabaseRoles on sql instances ServerA and ServerB\sql16. + +.EXAMPLE +Get-DbaDbRole -SqlInstance ServerB\sql16 -Database AccountingDB + +Returns SQLServer, Database, Role for DatabaseRoles in database AccountingDB on sql instance ServerB\sql16. + +.EXAMPLE +Get-DbaDbRole -SqlInstance ServerB\sql16 -ExcludeFixedRoles + +Returns SQLServer, Database, Role for DatabaseRoles on sql instance ServerB\sql16, but not the fixed roles. + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('SqlServer', 'ServerInstance')] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeFixedRole, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases | Where-Object IsAccessible + + if ($Database) { + Write-Message -Level Verbose -Message "Databases to check: $Database" + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + Write-Message -Level Verbose -Message "Databases excluded from check: $ExcludeDatabase" + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Checking accessibility of $db on $instance" + + if ($db.IsAccessible -ne $true) { + Write-Message -Level Warning -Message "Database $db on $instance is not accessible" + continue + } + + $dbroles = $db.roles + Write-Message -Level Verbose -Message "Getting Database Roles for $db on $instance" + + if ($ExcludeFixedRole) { + $dbroles = $dbroles | Where-Object IsFixedRole -eq $false + } + + foreach ($dbrole in $dbroles) { + Add-Member -Force -InputObject $dbrole -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $dbrole -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $dbrole -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $dbrole -MemberType NoteProperty -Name Database -value $db.Name + + Select-DefaultView -InputObject $dbrole -Property ComputerName, InstanceName, SqlInstance, Database, Name, Owner, CreateDate, DateLastModified, IsFixedRole + } + } + } + } +} +#ValidationTags#FlowControl# +function Get-DbaDbSnapshot { + <# + .SYNOPSIS + Get database snapshots with details + + .DESCRIPTION + Retrieves the list of database snapshot available, along with their base (the db they are the snapshot of) and creation time + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + Return information for only specific databases + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Snapshot + Return information for only specific snapshots + + .PARAMETER ExcludeSnapshot + The snapshot(s) to exclude - this list is auto-populated from the server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + + .LINK + https://dbatools.io/Get-DbaDbSnapshot + + .EXAMPLE + Get-DbaDbSnapshot -SqlInstance sqlserver2014a + + Returns a custom object displaying Server, Database, DatabaseCreated, SnapshotOf, SizeMB, DatabaseCreated + + .EXAMPLE + Get-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Returns information for database snapshots having HR and Accounting as base dbs + + .EXAMPLE + Get-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snapshot, Accounting_snapshot + + Returns information for database snapshots HR_snapshot and Accounting_snapshot + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Snapshot, + [object[]]$ExcludeSnapshot, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $alldbq = @" +SELECT sn.name as Name, dt.name as DatabaseSnapshotBaseName, sn.create_date as CreateDate, +CASE WHEN sn.source_database_id IS NOT NULL THEN 1 ELSE 0 END as IsDatabaseSnapshot +FROM sys.databases sn +LEFT JOIN sys.databases dt +ON sn.source_database_id = dt.database_id +WHERE sn.state <> 6 +"@ + $dbs = $server.Query($alldbq) + #$dbs = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.DatabaseSnapshotBaseName } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.DatabaseSnapshotBaseName } + } + if ($Snapshot) { + $dbs = $dbs | Where-Object { $Snapshot -contains $_.Name } + } + if (!$Snapshot -and !$Database) { + $dbs = $dbs | Where-Object IsDatabaseSnapshot -eq $true | Sort-Object DatabaseSnapshotBaseName, Name + } + if ($ExcludeSnapshot) { + $dbs = $dbs | Where-Object { $ExcludeSnapshot -notcontains $_.Name } + } + + foreach ($db in $dbs) { + $object = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + SnapshotOf = $db.DatabaseSnapshotBaseName + SizeMB = [Math]::Round($db.Size, 2) ##FIXME, should use the stats for sparse files + DatabaseCreated = [dbadatetime]$db.createDate + SnapshotDb = $server.Databases[$db.Name] + } + + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, Database, SnapshotOf, SizeMB, DatabaseCreated + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaDatabaseSnapshot + } +} + +function Get-DbaDbStoredProcedure { + <# + .SYNOPSIS + Gets database Stored Procedures + + .DESCRIPTION + Gets database Stored Procedures + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials + + .PARAMETER Database + To get Stored Procedures from specific database(s) + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server + + .PARAMETER ExcludeSystemSp + This switch removes all system objects from the Stored Procedure collection + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, StoredProcedure, Proc + Author: Klaas Vandenberghe ( @PowerDbaKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaDbStoredProcedure -SqlInstance sql2016 + + Gets all database Stored Procedures + + .EXAMPLE + Get-DbaDbStoredProcedure -SqlInstance Server1 -Database db1 + + Gets the Stored Procedures for the db1 database + + .EXAMPLE + Get-DbaDbStoredProcedure -SqlInstance Server1 -ExcludeDatabase db1 + + Gets the Stored Procedures for all databases except db1 + + .EXAMPLE + Get-DbaDbStoredProcedure -SqlInstance Server1 -ExcludeSystemSp + + Gets the Stored Procedures for all databases that are not system objects + + .EXAMPLE + 'Sql1','Sql2/sqlexpress' | Get-DbaDbStoredProcedure + + Gets the Stored Procedures for the databases on Sql1 and Sql2/sqlexpress + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$ExcludeSystemSp, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + if ($db.StoredProcedures.Count -eq 0) { + Write-Message -Message "No Stored Procedures exist in the $db database on $instance" -Target $db -Level Output + continue + } + + foreach ($proc in $db.StoredProcedures) { + if ( (Test-Bound -ParameterName ExcludeSystemSp) -and $proc.IsSystemObject ) { + continue + } + + Add-Member -Force -InputObject $proc -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $proc -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $proc -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $proc -MemberType NoteProperty -Name Database -value $db.Name + + $defaults = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Schema', 'ID as ObjectId', 'CreateDate', + 'DateLastModified', 'Name', 'ImplementationType', 'Startup' + Select-DefaultView -InputObject $proc -Property $defaults + } + } + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaDbVirtualLogFile { + <# + .SYNOPSIS + Returns database virtual log file information for database files on a SQL instance. + + .DESCRIPTION + Having a transaction log file with too many virtual log files (VLFs) can hurt database performance. + + Too many VLFs can cause transaction log backups to slow down and can also slow down database recovery and, in extreme cases, even affect insert/update/delete performance. + + References: + http://www.sqlskills.com/blogs/kimberly/transaction-log-vlfs-too-many-or-too-few/ + http://blogs.msdn.com/b/saponsqlserver/archive/2012/02/22/too-many-virtual-log-files-vlfs-can-cause-slow-database-recovery.aspx + + If you've got a high number of VLFs, you can use Expand-SqlTLogResponsibly to reduce the number. + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER IncludeSystemDBs + If this switch is enabled, system database information will be displayed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: VLF, Database, LogFile + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDbVirtualLogFile + + .EXAMPLE + Get-DbaDbVirtualLogFile -SqlInstance sqlcluster + + Returns all user database virtual log file details for the sqlcluster instance. + + .EXAMPLE + Get-DbaDbVirtualLogFile -SqlInstance sqlserver | Group-Object -Property Database | Where-Object Count -gt 50 + + Returns user databases that have 50 or more VLFs. + + .EXAMPLE + @('sqlserver','sqlcluster') | Get-DbaDbVirtualLogFile + + Returns all VLF information for the sqlserver and sqlcluster SQL Server instances. Processes data via the pipeline. + + .EXAMPLE + Get-DbaDbVirtualLogFile -SqlInstance sqlcluster -Database db1, db2 + + Returns the VLF counts for the db1 and db2 databases on sqlcluster. + #> + [CmdletBinding()] + [OutputType([System.Collections.ArrayList])] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystemDBs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $dbs = $dbs | Where-Object Name -in $Database + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + if (!$IncludeSystemDBs) { + $dbs = $dbs | Where-Object IsSystemObject -eq $false + } + + foreach ($db in $dbs) { + try { + $data = $db.Query("DBCC LOGINFO") + + foreach ($d in $data) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + RecoveryUnitId = $d.RecoveryUnitId + FileId = $d.FileId + FileSize = $d.FileSize + StartOffset = $d.StartOffset + FSeqNo = $d.FSeqNo + Status = $d.Status + Parity = $d.Parity + CreateLsn = $d.CreateLSN + } + } + } + catch { + Stop-Function -Message "Unable to query $($db.name) on $instance." -ErrorRecord $_ -Target $db -Continue + } + } + } + } +} +function Get-DbaDefaultPath { + <# + .SYNOPSIS + Gets the default SQL Server paths for data, logs and backups + + .DESCRIPTION + Gets the default SQL Server paths for data, logs and backups + + .PARAMETER SqlInstance + The SQL Server instance, or instances. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Config + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDefaultPath + + .EXAMPLE + Get-DbaDefaultPath -SqlInstance sql01\sharepoint + + Returns the default file paths for sql01\sharepoint + + .EXAMPLE + $servers = "sql2014","sql2016", "sqlcluster\sharepoint" + $servers | Get-DbaDefaultPath + + Returns the default file paths for "sql2014","sql2016" and "sqlcluster\sharepoint" + +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $datapath = $server.DefaultFile + + if ($datapath.Length -eq 0) { + $datapath = $server.ConnectionContext.ExecuteScalar("SELECT SERVERPROPERTY('InstanceDefaultDataPath')") + } + + <# eh, this can be a failback if we can get it to work ;) + if ($datapath -eq [System.DBNull]::Value -or $datapath.Length -eq 0) { + $datapath = $server.ConnectionContext.ExecuteScalar("master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'DefaultData'") + } + #> + + if ($datapath -eq [System.DBNull]::Value -or $datapath.Length -eq 0) { + $datapath = Split-Path (Get-DbaDatabase -SqlInstance $server -Database model).FileGroups[0].Files[0].FileName + } + + if ($datapath.Length -eq 0) { + $datapath = $server.Information.MasterDbPath + } + + $logpath = $server.DefaultLog + + if ($logpath.Length -eq 0) { + $logpath = $server.ConnectionContext.ExecuteScalar("SELECT SERVERPROPERTY('InstanceDefaultLogPath')") + } + + <# + eh, this can be a failback if we can get it to work ;) + right now, the Get-Database thing below causes enumeration which slows stuff down on systems with hundreds of dbs + if ($logpath -eq [System.DBNull]::Value -or $logpath.Length -eq 0) { + $logpath = $server.ConnectionContext.ExecuteScalar("master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'DefaultLog'") + } + #> + + if ($logpath -eq [System.DBNull]::Value -or $logpath.Length -eq 0) { + $logpath = Split-Path (Get-DbaDatabase -SqlInstance $server -Database model).LogFiles.FileName + } + + if ($logpath.Length -eq 0) { + $logpath = $server.Information.MasterDbLogPath + } + + $datapath = $datapath.Trim().TrimEnd("\") + $logpath = $logpath.Trim().TrimEnd("\") + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Data = $datapath + Log = $logpath + Backup = $server.BackupDirectory + ErrorLog = $server.ErrorLogPath + } + } + } +} +function Get-DbaDependency { + <# + .SYNOPSIS + Finds object dependencies and their relevant creation scripts. + + .DESCRIPTION + This function recursively finds all objects that depends on the input. + It will then retrieve rich information from them, including their creation scripts and the order in which it should be applied. + + By using the 'Parents' switch, the function will instead retrieve all items that the input depends on (including their creation scripts). + + For more details on dependency, see: + https://technet.microsoft.com/en-us/library/ms345449(v=sql.105).aspx + + .PARAMETER InputObject + The SMO object to parse + + .PARAMETER AllowSystemObjects + Normally, system objects are ignored by this function as dependencies. + This switch overrides that behavior. + + .PARAMETER Parents + Causes the function to retrieve all objects that the input depends on, rather than retrieving everything that depends on the input. + + .PARAMETER IncludeSelf + Includes the object whose dependencies are retrieves itself. + Useful when exporting an entire logic structure in order to recreate it in another database. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER IncludeScript + Setting this switch will cause the function to also retrieve the creation script of the dependency. + + .NOTES + Tags: Database, Dependent, Dependency, Object + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDependency + + .EXAMPLE + $table = (Get-DbaDatabase -SqlInstance sql2012 -Database Northwind).tables | Where Name -eq Customers + $table | Get-DbaDependency + + Returns everything that depends on the "Customers" table + #> + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline = $true)] + $InputObject, + + [switch] + $AllowSystemObjects, + + [switch] + $Parents, + + [switch] + $IncludeSelf, + + [switch] + [Alias('Silent')]$EnableException + ) + + Begin { + #region Utility functions + function Get-DependencyTree { + [CmdletBinding()] + Param ( + $Object, + + $Server, + + [bool] + $AllowSystemObjects, + + [bool] + $EnumParents, + + [string] + $FunctionName, + + [bool] + $EnableException + ) + + $scripter = New-Object Microsoft.SqlServer.Management.Smo.Scripter + $options = New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions + $options.DriAll = $true + $options.AllowSystemObjects = $AllowSystemObjects + $options.WithDependencies = $true + $scripter.Options = $options + $scripter.Server = $Server + + $urnCollection = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection + + Write-Message -EnableException $EnableException -Level 5 -Message "Adding $Object which is a $($Object.urn.Type)" -FunctionName $FunctionName + $urnCollection.Add([Microsoft.SqlServer.Management.Sdk.Sfc.Urn]$Object.urn) + + #now we set up an event listnenr go get progress reports + $progressReportEventHandler = [Microsoft.SqlServer.Management.Smo.ProgressReportEventHandler] { + $name = $_.Current.GetAttribute('Name'); + Write-Message -EnableException $EnableException -Level 5 -Message "Analysed $name" -FunctionName $FunctionName + } + $scripter.add_DiscoveryProgress($progressReportEventHandler) + + return $scripter.DiscoverDependencies($urnCollection, $EnumParents) + } + + function Read-DependencyTree { + [CmdletBinding()] + Param ( + [System.Object] + $InputObject, + + [int] + $Tier, + + [System.Object] + $Parent, + + [bool] + $EnumParents + ) + + Add-Member -Force -InputObject $InputObject -Name Parent -Value $Parent -MemberType NoteProperty + if ($EnumParents) { Add-Member -Force -InputObject $InputObject -Name Tier -Value ($Tier * -1) -MemberType NoteProperty -PassThru } + else { Add-Member -Force -InputObject $InputObject -Name Tier -Value $Tier -MemberType NoteProperty -PassThru } + + if ($InputObject.HasChildNodes) { Read-DependencyTree -InputObject $InputObject.FirstChild -Tier ($Tier + 1) -Parent $InputObject -EnumParents $EnumParents } + if ($InputObject.NextSibling) { Read-DependencyTree -InputObject $InputObject.NextSibling -Tier $Tier -Parent $Parent -EnumParents $EnumParents } + } + + function Get-DependencyTreeNodeDetail { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline = $true)] + $SmoObject, + + $Server, + + $OriginalResource, + + [bool] + $AllowSystemObjects + ) + + Begin { + $scripter = New-Object Microsoft.SqlServer.Management.Smo.Scripter + $options = New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions + $options.DriAll = $true + $options.AllowSystemObjects = $AllowSystemObjects + $options.WithDependencies = $true + $scripter.Options = $options + $scripter.Server = $Server + } + + process { + foreach ($Item in $SmoObject) { + $richobject = $Server.GetSmoObject($Item.urn) + $parent = $Server.GetSmoObject($Item.Parent.Urn) + + $NewObject = New-Object Sqlcollaborative.Dbatools.Database.Dependency + $NewObject.ComputerName = $server.NetName + $NewObject.ServiceName = $server.ServiceName + $NewObject.SqlInstance = $server.DomainInstanceName + $NewObject.Dependent = $richobject.Name + $NewObject.Type = $Item.Urn.Type + $NewObject.Owner = $richobject.Owner + $NewObject.IsSchemaBound = $Item.IsSchemaBound + $NewObject.Parent = $parent.Name + $NewObject.ParentType = $parent.Urn.Type + $NewObject.Tier = $Item.Tier + $NewObject.Object = $richobject + $NewObject.Urn = $richobject.Urn + $NewObject.OriginalResource = $OriginalResource + + $SQLscript = $scripter.EnumScriptWithList($richobject) + + # I can't remember how to remove these options and their syntax is breaking stuff + $SQLscript = $SQLscript -replace "SET ANSI_NULLS ON", "" + $SQLscript = $SQLscript -replace "SET QUOTED_IDENTIFIER ON", "" + $NewObject.Script = "$SQLscript `r`ngo" + + $NewObject + } + } + } + + function Select-DependencyPrecedence { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline = $true)] + $Dependency + ) + + Begin { + $list = @() + } + Process { + foreach ($dep in $Dependency) { + # Killing the pipeline is generally a bad idea, but since we have to group and sort things, we have not really a choice + $list += $dep + } + } + End { + $list | Group-Object -Property Object | ForEach-Object { $_.Group | Sort-Object -Property Tier -Descending | Select-Object -First 1 } | Sort-Object Tier + } + } + #endregion Utility functions + } + Process { + foreach ($Item in $InputObject) { + Write-Message -EnableException $EnableException -Level 5 -Message "Processing: $Item" + if ($null -eq $Item.urn) { + Stop-Function -Message "$Item is not a valid SMO object" -EnableException $EnableException -Category InvalidData -Continue -Target $Item + } + + # Find the server object to pass on to the function + $parent = $Item.parent + + do { $parent = $parent.parent } + until (($parent.urn.type -eq "Server") -or (-not $parent)) + + if (-not $parent) { + Stop-Function -Message "Failed to find valid server object in input: $Item" -EnableException $EnableException -Category InvalidData -Continue -Target $Item + } + + $server = $parent + + $tree = Get-DependencyTree -Object $Item -AllowSystemObjects $false -Server $server -FunctionName (Get-PSCallStack)[0].COmmand -EnableException $EnableException -EnumParents $Parents + $limitCount = 2 + if ($IncludeSelf) { $limitCount = 1 } + if ($tree.Count -lt $limitCount) { + Write-Message -Message "No dependencies detected for $($Item)" -Level 2 -EnableException $EnableException + continue + } + + if ($IncludeSelf) { $resolved = Read-DependencyTree -InputObject $tree.FirstChild -Tier 0 -Parent $tree.FirstChild -EnumParents $Parents } + else { $resolved = Read-DependencyTree -InputObject $tree.FirstChild.FirstChild -Tier 1 -Parent $tree.FirstChild -EnumParents $Parents } + $resolved | Get-DependencyTreeNodeDetail -Server $server -OriginalResource $Item -AllowSystemObjects $AllowSystemObjects | Select-DependencyPrecedence + } + } +} +function Get-DbaDetachedDatabaseInfo { + <# + .SYNOPSIS + Get detailed information about detached SQL Server database files. + + .DESCRIPTION + Gathers the following information from detached database files: database name, SQL Server version (compatibility level), collation, and file structure. + + "Data files" and "Log file" report the structure of the data and log files as they were when the database was detached. "Database version" is the compatibility level. + + MDF files are most easily read by using a SQL Server to interpret them. Because of this, you must specify a SQL Server and the path must be relative to the SQL Server. + + .PARAMETER SqlInstance + Source SQL Server. This instance must be online and is required to parse the information contained with in the detached database file. + + This function will not attach the database file, it will only use SQL Server to read its contents. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the path to the MDF file to be read. This path must be readable by the SQL Server service account. Ideally, the MDF will be located on the SQL Server itself, or on a network share to which the SQL Server service account has access. + + .NOTES + Tags: Database, Detach + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDetachedDatabaseInfo + + .EXAMPLE + Get-DbaDetachedDatabaseInfo -SqlInstance sql2016 -Path M:\Archive\mydb.mdf + + Returns information about the detached database file M:\Archive\mydb.mdf using the SQL Server instance sql2016. The M drive is relative to the SQL Server instance. + #> + + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(Mandatory = $true)] + [Alias("Mdf")] + [string]$Path, + [PSCredential]$SqlCredential + ) + + begin { + function Get-MdfFileInfo { + $datafiles = New-Object System.Collections.Specialized.StringCollection + $logfiles = New-Object System.Collections.Specialized.StringCollection + + $servername = $server.name + $serviceaccount = $server.ServiceAccount + + $exists = Test-DbaSqlPath -SqlInstance $server -Path $Path + + if ($exists -eq $false) { + throw "$servername cannot access the file $path. Does the file exist and does the service account ($serviceaccount) have access to the path?" + } + + try { + $detachedDatabaseInfo = $server.DetachedDatabaseInfo($path) + $dbname = ($detachedDatabaseInfo | Where-Object { $_.Property -eq "Database name" }).Value + $exactdbversion = ($detachedDatabaseInfo | Where-Object { $_.Property -eq "Database version" }).Value + $collationid = ($detachedDatabaseInfo | Where-Object { $_.Property -eq "Collation" }).Value + } + catch { + throw "$servername cannot read the file $path. Is the database detached?" + } + + switch ($exactdbversion) { + 852 { $dbversion = "SQL Server 2016" } + 829 { $dbversion = "SQL Server 2016 Prerelease" } + 782 { $dbversion = "SQL Server 2014" } + 706 { $dbversion = "SQL Server 2012" } + 684 { $dbversion = "SQL Server 2012 CTP1" } + 661 { $dbversion = "SQL Server 2008 R2" } + 660 { $dbversion = "SQL Server 2008 R2" } + 655 { $dbversion = "SQL Server 2008 SP2+" } + 612 { $dbversion = "SQL Server 2005" } + 611 { $dbversion = "SQL Server 2005" } + 539 { $dbversion = "SQL Server 2000" } + 515 { $dbversion = "SQL Server 7.0" } + 408 { $dbversion = "SQL Server 6.5" } + default { $dbversion = "Unknown" } + } + + $collationsql = "SELECT name FROM fn_helpcollations() where collationproperty(name, N'COLLATIONID') = $collationid" + + try { + $dataset = $server.databases['master'].ExecuteWithResults($collationsql) + $collation = "$($dataset.Tables[0].Rows[0].Item(0))" + } + catch { + $collation = $collationid + } + + if ($collation.length -eq 0) { $collation = $collationid } + + try { + foreach ($file in $server.EnumDetachedDatabaseFiles($path)) { + $datafiles += $file + } + + foreach ($file in $server.EnumDetachedLogFiles($path)) { + $logfiles += $file + } + } + catch { + throw "$servername unable to enumerate database or log structure information for $path" + } + + $mdfinfo = [pscustomobject]@{ + Name = $dbname + Version = $dbversion + ExactVersion = $exactdbversion + Collation = $collation + DataFiles = $datafiles + LogFiles = $logfiles + } + + return $mdfinfo + } + } + + process { + + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + $mdfinfo = Get-MdfFileInfo $server $path + + } + + end { + $server.ConnectionContext.Disconnect() + return $mdfinfo + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaDiskSpace { + <# + .SYNOPSIS + Displays disk information for all local disk on a server. + + .DESCRIPTION + Returns a custom object with server name, name of disk, label of disk, total size, free size, percent free, block size and filesystem. + + By default, this function only shows drives of types 2 and 3 (removable disk and local disk). + + Requires Windows administrator access on SQL Servers + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER Unit + This parameter has been deprecated and will be removed in 1.0.0 + All properties previously generated through this command are present at the same time, but hidden by default. + + .PARAMETER CheckForSql + If this switch is enabled, disks will be checked for SQL Server data and log files. Windows Authentication is always used for this. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ExcludeDrive + Filter out drives - format is C:\ + + .PARAMETER Detailed + Output all properties, will be deprecated in 1.0.0 release. Use Force Instead + + .PARAMETER CheckFragmentation + If this switch is enabled, fragmentation of all filesystems will be checked. + + This will increase the runtime of the function by seconds or even minutes per volume. + + .PARAMETER Force + Enabling this switch will cause the command to include ALL drives. + By default, only local disks and removable disks are shown, and hidden volumes are excluded. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: Storage, Disk + Author: Chrissy LeMaire (clemaire@gmail.com) & Jakob Bindslet (jakob@bindslet.dk) + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDiskSpace + + .EXAMPLE + Get-DbaDiskSpace -ComputerName srv0042 + + Get disk space for the server srv0042. + + .EXAMPLE + Get-DbaDiskSpace -ComputerName srv0042 -Unit MB + + Get disk space for the server srv0042 and displays in megabytes (MB). + + .EXAMPLE + Get-DbaDiskSpace -ComputerName srv0042, srv0007 -Unit TB + + Get disk space from two servers and displays in terabytes (TB). + + .EXAMPLE + Get-DbaDiskSpace -ComputerName srv0042 -Force + + Get all disk and volume space information. + + .EXAMPLE + Get-DbaDiskSpace -ComputerName srv0042 -ExcludeDrive 'C:\' + + Get all disk and volume space information. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlInstance', 'SqlServer')] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [ValidateSet('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB')] + [string]$Unit = 'GB', + [switch]$CheckForSql, + [PSCredential]$SqlCredential, + [string[]]$ExcludeDrive, + [Alias('Detailed', 'AllDrives')] + [switch]$CheckFragmentation, + [switch]$Force, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Parameter Detailed + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Parameter AllDrives + Test-DbaDeprecation -DeprecatedOn 1.0.0 -Parameter Unit + + $condition = " WHERE DriveType = 2 OR DriveType = 3" + if (Test-Bound 'Force') { + $condition = "" + } + + # Keep track of what computer was already processed to avoid duplicates + $processed = New-Object System.Collections.ArrayList + + <# In order to support properly identifying if a disk/volume is involved with ANY instance on a given computer #> + $sqlDisks = New-Object System.Collections.ArrayList + } + + process { + foreach ($computer in $ComputerName) { + if ($computer.ComputerName -notin $processed) { + $null = $processed.Add($computer.ComputerName) + Write-Message -Level VeryVerbose -Message "Connecting to $computer." -Target $computer.ComputerName + } + else { + continue + } + + try { + $disks = Get-DbaCmObject -ComputerName $computer.ComputerName -Query "SELECT * FROM Win32_Volume$condition" -Credential $Credential -Namespace root\CIMv2 -ErrorAction Stop -WarningAction SilentlyContinue -EnableException + } + catch { + Stop-Function -Message "Failed to connect to $computer." -EnableException $EnableException -ErrorRecord $_ -Target $computer.ComputerName -Continue + } + + foreach ($disk in $disks) { + if ($disk.Name -in $ExcludeDrive) { + continue + } + if ($disk.Name.StartsWith('\\') -and (-not $Force)) { + Write-Message -Level Verbose -Message "Skipping disk: $($disk.Name)" -Target $computer.ComputerName + continue + } + + Write-Message -Level Verbose -Message "Processing disk: $($disk.Name)" -Target $computer.ComputerName + + $info = New-Object Sqlcollaborative.Dbatools.Computer.DiskSpace + $info.ComputerName = $computer.ComputerName + $info.Name = $disk.Name + $info.Label = $disk.Label + $info.Capacity = $disk.Capacity + $info.Free = $disk.Freespace + $info.BlockSize = $disk.BlockSize + $info.FileSystem = $disk.FileSystem + $info.Type = $disk.DriveType + + if ($CheckForSql) { + $driveLetter = $disk.DriveLetter.TrimEnd(":") + try { + $sqlServices = Get-DbaSqlService -ComputerName $computer -Type Engine + } + catch { + Write-Message -Level Warning -Message "Failed to connect to $computer to gather SQL Server instances, will not be reporting SQL Information." -ErrorRecord $_ -OverrideExceptionMessage -Target $computer.ComputerName + } + + Write-Message -Level Verbose -Message "Instances found on $($computer): $($sqlServices.InstanceName.Count)" + if ($sqlServices.InstanceName.Count -gt 0) { + foreach ($sqlService in $sqlServices) { + if ($sqlService.InstanceName -eq "MSSQLSERVER") { + $instanceName = $sqlService.ComputerName + } + else { + $instanceName = "$($sqlService.ComputerName)\$($sqlService.InstanceName)" + } + Write-Message -Level VeryVerbose -Message "Processing instance $($instanceName)" + try { + $server = Connect-SqlInstance -SqlInstance $instanceName -SqlCredential $SqlCredential + if ($server.Version -lt 9) { + $sql = "SELECT DISTINCT LEFT(filename,1) AS SqlDisk FROM sysaltfiles" + } + else { + $sql = "SELECT DISTINCT LEFT(physical_name,1) AS SqlDisk FROM sys.master_files" + } + $results = $server.Query($sql) + if ($results.SqlDisk.Count -gt 0) { + foreach ($sqlDisk in $results.SqlDisk) { + $null = $sqlDisks.Add($sqlDisk) + } + } + } + catch { + Write-Message -Level Warning -Message "Failed to connect to $instanceName on $computer. SQL information may not be accurate or services have been stopped." -ErrorRecord $_ -OverrideExceptionMessage -Target $computer.ComputerName + } + } + } + $info.IsSqlDisk = ($driveLetter -in $sqlDisks) + $info + } + $info + } + } + } +} +function Get-DbaDistributor { + <# + .SYNOPSIS + Gets the information about a replication distributor for a given SQL Server instance. + + .DESCRIPTION + This function locates and enumerates distributor information for a given SQL Server instance. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: William Durkin, @sql_williamd + Tags: Replication + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDistributor + + .EXAMPLE + Get-DbaDistributor -SqlInstance sql2008, sqlserver2012 + Retrieve distributor information for servers sql2008 and sqlserver2012. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(Position = 1)] + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ($null -eq [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.RMO")) { + Stop-Function -Message "Replication management objects not available. Please install SQL Server Management Studio." + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + # connect to the instance + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Verbose -Message "Attempting to retrieve distributor information from $instance" + + # Connect to the distributor of the instance + try { + $sourceSqlConn = $server.ConnectionContext.SqlConnectionObject + $distributor = New-Object Microsoft.SqlServer.Replication.ReplicationServer $sourceSqlConn + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Add-Member -Force -InputObject $distributor -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $distributor -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $distributor -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + + Select-DefaultView -InputObject $distributor -Property ComputerName, InstanceName, SqlInstance, IsPublisher, IsDistributor, DistributionServer, DistributionDatabase, DistributorInstalled, DistributorAvailable, HasRemotePublisher + } + } +} +function Get-DbaDump { + <# + .SYNOPSIS + Locate a SQL Server that has generated any memory dump files. + + .DESCRIPTION + The type of dump included in the search include minidump, all-thread dump, or a full dump. The files have an extendion of .mdmp. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Engine, Corruption + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaDump + + .EXAMPLE + Get-DbaDump -SqlInstance sql2016 + + Shows the detailed information for memory dump(s) located on sql2016 instance + + + .EXAMPLE + Get-DbaDump -SqlInstance sql2016 -SqlCredential (Get-Credential sqladmin) + + Shows the detailed information for memory dump(s) located on sql2016 instance. Logs into the SQL Server using the SQL login 'sqladmin' + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + begin { + $sql = "SELECT filename, creation_time, size_in_bytes FROM sys.dm_server_memory_dumps" + } + + process { + foreach ($instance in $SqlInstance) { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + + if ($server.versionMajor -lt 11 -and (-not ($server.versionMajor -eq 10 -and $server.versionMinor -eq 50))) { + Stop-Function -Message "This function does not support versions lower than SQL Server 2008 R2 (v10.50). Skipping server '$instance'" -Continue + } + + try { + foreach ($result in $server.Query($sql)) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + FileName = $result.filename + CreationTime = $result.creation_time + Size = [dbasize]$result.size_in_bytes + } + } + } + catch { + Stop-Function -Message "Issue collecting data on $server" -Target $server -ErrorRecord $_ -Continue + } + } + } +} +#ValidationTags#Messaging,FlowControl,CodeStyle# +function Get-DbaEndpoint { + <# + .SYNOPSIS + Gets SQL Endpoint(s) information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaEndpoint command gets SQL Endpoint(s) information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Endpoint + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaEndpoint + + .EXAMPLE + Get-DbaEndpoint -SqlInstance localhost + + Returns all Endpoint(s) on the local default SQL Server instance + + .EXAMPLE + Get-DbaEndpoint -SqlInstance localhost, sql2016 + + Returns all Endpoint(s) for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($endpoint in $server.Endpoints) { + Add-Member -Force -InputObject $endpoint -MemberType NoteProperty -Name ComputerName -value $endpoint.Parent.NetName + Add-Member -Force -InputObject $endpoint -MemberType NoteProperty -Name InstanceName -value $endpoint.Parent.ServiceName + Add-Member -Force -InputObject $endpoint -MemberType NoteProperty -Name SqlInstance -value $endpoint.Parent.DomainInstanceName + + Select-DefaultView -InputObject $endpoint -Property ComputerName, InstanceName, SqlInstance, ID, Name, EndpointType, Owner, IsAdminEndpoint, IsSystemObject + } + } + } +} +function Get-DbaErrorLog { + <# + .SYNOPSIS + Gets the "SQL Error Log" of an instance + + .DESCRIPTION + Gets the "SQL Error Log" of an instance. Returns all 10 error logs by default. + + .PARAMETER SqlInstance + The SQL Server instance, or instances. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER LogNumber + An Int32 value that specifies the index number of the error log required. + Error logs are listed 0 through 99, where 0 is the current error log and 99 is potential oldest log file. + + SQL Server errorlog rollover defaults to 6, but can be increased to 99. https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/scm-services-configure-sql-server-error-logs + + .PARAMETER Source + Filter results based on the Source of the error (e.g. Logon, Server, etc.) + + .PARAMETER Text + Filter results based on a pattern of text (e.g. "login failed", "error: 12345"). + + .PARAMETER After + Filter the results based on datetime value. + + .PARAMETER Before + Filter the results based on datetime value. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, ErrorLog + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaErrorLog + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint + + Returns every log entry from sql01\sharepoint SQL Server instance. + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint -LogNumber 3, 6 + + Returns all log entries for log number 3 and 6 on sql01\sharepoint SQL Server instance. + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint -Source Logon + + Returns every log entry, with a source of Logon, from sql01\sharepoint SQL Server instance. + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint -LogNumber 3 -Text "login failed" + + Returns every log entry for log number 3, with "login failed" in the text, from sql01\sharepoint SQL Server instance. + + .EXAMPLE + $servers = "sql2014","sql2016", "sqlcluster\sharepoint" + $servers | Get-DbaErrorLog -LogNumber 0 + + Returns the most recent SQL Server error logs for "sql2014","sql2016" and "sqlcluster\sharepoint" + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint -After '11/14/2006 00:00' + + Returns every log entry found after the date 11/14/2006 00:00 from sql101\sharepoint SQL Server instance. + + .EXAMPLE + Get-DbaErrorLog -SqlInstance sql01\sharepoint -Before '08/16/2016 00:00' + + Returns every log entry found before the date 08/16/2016 00:00 from sql101\sharepoint SQL Server instance. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateRange(0, 99)] + [int[]]$LogNumber, + [object[]]$Source, + [string]$Text, + [datetime]$After, + [datetime]$Before, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaSqlLog + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($LogNumber) { + foreach ($number in $lognumber) { + foreach ($object in $server.ReadErrorLog($number)) { + if ( ($Source -and $object.ProcessInfo -ne $Source) -or ($Text -and $object.Text -notlike "*$Text*") -or ($After -and $object.LogDate -lt $After) -or ($Before -and $object.LogDate -gt $Before) ) { + continue + } + Write-Message -Level Verbose -Message "Processing $object" + Add-Member -Force -InputObject $object -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $object -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $object -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + # Select all of the columns you'd like to show + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, LogDate, 'ProcessInfo as Source', Text + } + } + } + else { + foreach ($object in $server.ReadErrorLog()) { + if ( ($Source -and $object.ProcessInfo -ne $Source) -or ($Text -and $object.Text -notlike "*$Text*") -or ($After -and $object.LogDate -lt $After) -or ($Before -and $object.LogDate -gt $Before) ) { + continue + } + Write-Message -Level Verbose -Message "Processing $object" + Add-Member -Force -InputObject $object -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $object -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $object -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + # Select all of the columns you'd like to show + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, LogDate, 'ProcessInfo as Source', Text + } + } + } + } +} +function Get-DbaErrorLogConfig { + <# + .SYNOPSIS + Pulls the configuration for the ErrorLog on a given SQL Server instance + .DESCRIPTION + Pulls the configuration for the ErrorLog on a given SQL Server instance. + Includes number of log files configured and size in KB (SQL Server 2012+ only) + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, ErrorLog + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: GNU GPL v3 https://opensource.org/licenses/GPL-3.0 + + .LINK + https://dbatools.io/Get-DbaErrorLogConfig + + .EXAMPLE + Get-DbaErrorLogConfig -SqlInstance server2017,server2014 + + Returns error log configuration for server2017 and server2014 + #> + [cmdletbinding()] + param( + [Parameter(ValueFromPipelineByPropertyName)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $numLogs = $server.NumberOfLogFiles + $logSize = + if ($server.VersionMajor -ge 11) { + $server.ErrorLogSizeKb + } + else { + $null + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + NumberErrorLogs = $numLogs + ErrorLogSizeKb = $logSize + } + } + } +} +function Get-DbaEstimatedCompletionTime { + <# +.SYNOPSIS +Gets execution and estimated completion time information for queries + +.DESCRIPTION +Gets execution and estimated completion time information for queries + +Percent complete will show for the following commands + +ALTER INDEX REORGANIZE +AUTO_SHRINK option with ALTER DATABASE +BACKUP DATABASE +DBCC CHECKDB +DBCC CHECKFILEGROUP +DBCC CHECKTABLE +DBCC INDEXDEFRAG +DBCC SHRINKDATABASE +DBCC SHRINKFILE +RECOVERY +RESTORE DATABASE +ROLLBACK +TDE ENCRYPTION + +For additional information, check out https://blogs.sentryone.com/loriedwards/patience-dm-exec-requests/ and https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-exec-requests-transact-sql + +.PARAMETER SqlInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +SqlCredential object used to connect to the SQL Server as a different user. + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaEstimatedCompletionTime + +.EXAMPLE +Get-DbaEstimatedCompletionTime -SqlInstance sql2016 + +Gets estimated completion times for queries performed against the entire server + +.EXAMPLE +Get-DbaEstimatedCompletionTime -SqlInstance sql2016 | Select * + +Gets estimated completion times for queries performed against the entire server PLUS the SQL query text of each command + +.EXAMPLE +Get-DbaEstimatedCompletionTime -SqlInstance sql2016 | Where-Object { $_.Text -match 'somequerytext' } + +Gets results for commands whose queries only match specific text (match is like LIKE but way more powerful) + +.EXAMPLE +Get-DbaEstimatedCompletionTime -SqlInstance sql2016 -Database Northwind,pubs,Adventureworks2014 + +Gets estimated completion times for queries performed against the Northwind, pubs, and Adventureworks2014 databases + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = "SELECT + DB_NAME(r.database_id) as [Database], + USER_NAME(r.user_id) as [Login], + Command, + start_time as StartTime, + percent_complete as PercentComplete, + + RIGHT('00000' + CAST(((DATEDIFF(s,start_time,GetDate()))/3600) as varchar), + CASE + WHEN LEN(((DATEDIFF(s,start_time,GetDate()))/3600)) < 2 THEN 2 + ELSE LEN(((DATEDIFF(s,start_time,GetDate()))/3600)) + END) + ':' + + RIGHT('00' + CAST((DATEDIFF(s,start_time,GetDate())%3600)/60 as varchar), 2) + ':' + + RIGHT('00' + CAST((DATEDIFF(s,start_time,GetDate())%60) as varchar), 2) as RunningTime, + + RIGHT('00000' + CAST((estimated_completion_time/3600000) as varchar), + CASE + WHEN LEN((estimated_completion_time/3600000)) < 2 THEN 2 + ELSE LEN((estimated_completion_time/3600000)) + END) + ':' + + RIGHT('00' + CAST((estimated_completion_time %3600000)/60000 as varchar), 2) + ':' + + RIGHT('00' + CAST((estimated_completion_time %60000)/1000 as varchar), 2) as EstimatedTimeToGo, + dateadd(second,estimated_completion_time/1000, getdate()) as EstimatedCompletionTime, + s.Text + FROM sys.dm_exec_requests r + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) s" + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $includedatabases = $Database -join "','" + $sql = "$sql WHERE DB_NAME(r.database_id) in ('$includedatabases')" + } + + if ($ExcludeDatabase) { + $excludedatabases = $ExcludeDatabase -join "','" + $sql = "$sql WHERE DB_NAME(r.database_id) not in ('$excludedatabases')" + } + + Write-Message -Level Debug -Message $sql + foreach ($row in ($server.Query($sql))) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.Database + Login = $row.Login + Command = $row.Command + PercentComplete = $row.PercentComplete + StartTime = $row.StartTime + RunningTime = $row.RunningTime + EstimatedTimeToGo = $row.EstimatedTimeToGo + EstimatedCompletionTime = $row.EstimatedCompletionTime + Text = $row.Text + } | Select-DefaultView -ExcludeProperty Text + } + } + } +} + +function Get-DbaExecutionPlan { + <# +.SYNOPSIS +Gets execution plans and metadata + +.DESCRIPTION +Gets execution plans and metadata. Can pipe to Export-DbaExecutionPlan :D + +Thanks to + https://www.simple-talk.com/sql/t-sql-programming/dmvs-for-query-plan-metadata/ + and + http://www.scarydba.com/2017/02/13/export-plans-cache-sqlplan-file/ +for the idea and query. + +.PARAMETER SqlInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER Database +Return restore information for only specific databases. These are only the databases that currently exist on the server. + +.PARAMETER ExcludeDatabase +Return restore information for all but these specific databases + +.PARAMETER SinceCreation +Datetime object used to narrow the results to a date + +.PARAMETER SinceLastExecution +Datetime object used to narrow the results to a date + +.PARAMETER ExcludeEmptyQueryPlan +Exclude results with empty query plan + +.PARAMETER Force +Returns a ton of raw information about the execution plans + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + +.NOTES +Tags: Performance +dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) +Copyright (C) 2016 Chrissy LeMaire +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaExecutionPlan + +.EXAMPLE +Get-DbaExecutionPlan -SqlInstance sqlserver2014a + +Gets all execution plans on sqlserver2014a + +.EXAMPLE +Get-DbaExecutionPlan -SqlInstance sqlserver2014a -Database db1, db2 -SinceLastExecution '7/1/2016 10:47:00' + +Gets all execution plans for databases db1 and db2 on sqlserver2014a since July 1, 2016 at 10:47 AM. + +.EXAMPLE +Get-DbaExecutionPlan -SqlInstance sqlserver2014a, sql2016 -Exclude db1 | Format-Table + +Gets execution plan info for all databases except db1 on sqlserver2014a and sql2016 and makes the output pretty + +.EXAMPLE +Get-DbaExecutionPlan -SqlInstance sql2014 -Database AdventureWorks2014, pubs -Force + +Gets super detailed information for execution plans on only for AdventureWorks2014 and pubs + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$SinceCreation, + [datetime]$SinceLastExecution, + [switch]$ExcludeEmptyQueryPlan, + [switch]$Force, + [switch]$EnableException + ) + + begin { + + if ($SinceCreation -ne $null) { + $SinceCreation = $SinceCreation.ToString("yyyy-MM-dd HH:mm:ss") + } + + if ($SinceLastExecution -ne $null) { + $SinceLastExecution = $SinceLastExecution.ToString("yyyy-MM-dd HH:mm:ss") + } + } + process { + + foreach ($instance in $sqlinstance) { + try { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($force -eq $true) { + $select = "SELECT * " + } + else { + $select = "SELECT DB_NAME(deqp.dbid) as DatabaseName, OBJECT_NAME(deqp.objectid) as ObjectName, + detqp.query_plan AS SingleStatementPlan, + deqp.query_plan AS BatchQueryPlan, + ROW_NUMBER() OVER ( ORDER BY Statement_Start_offset ) AS QueryPosition, + sql_handle as SqlHandle, + plan_handle as PlanHandle, + creation_time as CreationTime, + last_execution_time as LastExecutionTime" + } + + $from = " FROM sys.dm_exec_query_stats deqs + CROSS APPLY sys.dm_exec_text_query_plan(deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset) AS detqp + CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) AS deqp + CROSS APPLY sys.dm_exec_sql_text(deqs.plan_handle) AS execText" + + if ($ExcludeDatabase -or $Database -or $SinceCreation.length -gt 0 -or $SinceLastExecution.length -gt 0 -or $ExcludeEmptyQueryPlan -eq $true) { + $where = " WHERE " + } + + $wherearray = @() + + if ($Database) { + $dblist = $Database -join "','" + $wherearray += " DB_NAME(deqp.dbid) in ('$dblist') " + } + + if ($null -ne $SinceCreation) { + Write-Message -Level Verbose -Message "Adding creation time" + $wherearray += " creation_time >= '$SinceCreation' " + } + + if ($null -ne $SinceLastExecution) { + Write-Message -Level Verbose -Message "Adding last exectuion time" + $wherearray += " last_execution_time >= '$SinceLastExecution' " + } + + if ($ExcludeDatabase) { + $dblist = $ExcludeDatabase -join "','" + $wherearray += " DB_NAME(deqp.dbid) not in ('$dblist') " + } + + if ($ExcludeEmptyQueryPlan) { + $wherearray += " detqp.query_plan is not null" + } + + if ($where.length -gt 0) { + $wherearray = $wherearray -join " and " + $where = "$where $wherearray" + } + + $sql = "$select $from $where" + Write-Message -Level Debug -Message $sql + + if ($Force -eq $true) { + $server.Query($sql) + } + else { + foreach ($row in $server.Query($sql)) { + $simple = ([xml]$row.SingleStatementPlan).ShowPlanXML.BatchSequence.Batch.Statements.StmtSimple + $sqlhandle = "0x"; $row.sqlhandle | ForEach-Object { $sqlhandle += ("{0:X}" -f $_).PadLeft(2, "0") } + $planhandle = "0x"; $row.planhandle | ForEach-Object { $planhandle += ("{0:X}" -f $_).PadLeft(2, "0") } + $planWarnings = $simple.QueryPlan.Warnings.PlanAffectingConvert; + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $row.DatabaseName + ObjectName = $row.ObjectName + QueryPosition = $row.QueryPosition + SqlHandle = $SqlHandle + PlanHandle = $PlanHandle + CreationTime = $row.CreationTime + LastExecutionTime = $row.LastExecutionTime + StatementCondition = ([xml]$row.SingleStatementPlan).ShowPlanXML.BatchSequence.Batch.Statements.StmtCond + StatementSimple = $simple + StatementId = $simple.StatementId + StatementCompId = $simple.StatementCompId + StatementType = $simple.StatementType + RetrievedFromCache = $simple.RetrievedFromCache + StatementSubTreeCost = $simple.StatementSubTreeCost + StatementEstRows = $simple.StatementEstRows + SecurityPolicyApplied = $simple.SecurityPolicyApplied + StatementOptmLevel = $simple.StatementOptmLevel + QueryHash = $simple.QueryHash + QueryPlanHash = $simple.QueryPlanHash + StatementOptmEarlyAbortReason = $simple.StatementOptmEarlyAbortReason + CardinalityEstimationModelVersion = $simple.CardinalityEstimationModelVersion + + ParameterizedText = $simple.ParameterizedText + StatementSetOptions = $simple.StatementSetOptions + QueryPlan = $simple.QueryPlan + BatchConditionXml = ([xml]$row.BatchQueryPlan).ShowPlanXML.BatchSequence.Batch.Statements.StmtCond + BatchSimpleXml = ([xml]$row.BatchQueryPlan).ShowPlanXML.BatchSequence.Batch.Statements.StmtSimple + BatchQueryPlanRaw = [xml]$row.BatchQueryPlan + SingleStatementPlanRaw = [xml]$row.SingleStatementPlan + PlanWarnings = $planWarnings + } | Select-DefaultView -ExcludeProperty BatchQueryPlan, SingleStatementPlan, BatchConditionXmlRaw, BatchQueryPlanRaw, SingleStatementPlanRaw, PlanWarnings + } + } + } + catch { + Stop-Function -Message "Query Failure Failure" -ErrorRecord $_ -Target $instance -Continue + } + } + } +} +function Get-DbaFile { + <# +.SYNOPSIS +Get-DbaFile finds files in any directory specified on a remote SQL Server + +.DESCRIPTION +This command searches all specified directories, allowing a DBA to see file information on a server without direct access + +You can filter by extension using the -FileType parameter. By default, the default data directory will be returned. You can provide and additional paths to search using the -Path parameter. + +Thanks to serg-52 for the query: https://www.sqlservercentral.com/Forums/Topic1642213-391-1.aspx + +.PARAMETER SqlInstance +The SQL Server instance. + +.PARAMETER SqlCredential +Allows you to login to servers using alternative credentials + +.PARAMETER Path +Used to specify extra directories to search in addition to the default data directory. + +.PARAMETER FileType +Used to specify filter by filetype. No dot required, just pass the extension. + +.PARAMETER Depth +Used to specify recursive folder depth. Default is 1, non-recursive. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Discovery +Author: Brandon Abshire, netnerds.net + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaFile + +.EXAMPLE +Get-DbaFile -SqlInstance sqlserver2014a -Path E:\Dir1 +Logs into the SQL Server "sqlserver2014a" using Windows credentials and searches E:\Dir for all files + +.EXAMPLE +Get-DbaFile -SqlInstance sqlserver2014a -SqlCredential $cred -Path 'E:\sql files' +Logs into the SQL Server "sqlserver2014a" using alternative credentials and returns all files in 'E:\sql files' + +.EXAMPLE +$all = Get-DbaDefaultPath -SqlInstance sql2014 +Get-DbaFile -SqlInstance sql2014 -Path $all.Data, $all.Log, $all.Backup -Depth 3 +Returns the files in the default data, log and backup directories on sql2014, 3 directories deep (recursively). + +.EXAMPLE +Get-DbaFile -SqlInstance sql2014 -Path 'E:\Dir1', 'E:\Dir2' +Returns the files in "E:\Dir1" and "E:Dir2" on sql2014 + +.EXAMPLE +Get-DbaFile -SqlInstance -Path 'E:\Dir1' sql2014, sql2016 -FileType fsf, mld +Finds files in E:\Dir1 ending with ".fsf" and ".mld" for both the servers sql2014 and sql2016. + +.EXAMPLE +Get-DbaFile -SqlInstance -Path 'E:\Dir1' sql2014, sql2016 -FileType fsf, mld +Finds files in E:\Dir1 ending with ".fsf" and ".mld" for both the servers sql2014 and sql2016. +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Path, + [string[]]$FileType, + [int]$Depth = 1, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $sql = "" + + function Get-SQLDirTreeQuery { + param + ( + $PathList + ) + + $q1 += "DECLARE @myPath nvarchar(4000); + DECLARE @depth SMALLINT = $Depth; + + IF OBJECT_ID('tempdb..#DirectoryTree') IS NOT NULL + DROP TABLE #DirectoryTree; + + CREATE TABLE #DirectoryTree ( + id int IDENTITY(1,1) + ,subdirectory nvarchar(512) + ,depth int + ,isfile bit + , ParentDirectory int + ,flag tinyint default(0));" + + $q2 = "SET @myPath = 'dirname' + -- top level directory + INSERT #DirectoryTree (subdirectory,depth,isfile) + VALUES (@myPath,0,0); + -- all the rest under top level + INSERT #DirectoryTree (subdirectory,depth,isfile) + EXEC master.sys.xp_dirtree @myPath,@depth,1; + + + UPDATE #DirectoryTree + SET ParentDirectory = ( + SELECT MAX(Id) FROM #DirectoryTree + WHERE Depth = d.Depth - 1 AND Id < d.Id ) + FROM #DirectoryTree d + WHERE ParentDirectory is NULL;" + + $query_files_sql = "-- SEE all with full paths + WITH dirs AS ( + SELECT + Id,subdirectory,depth,isfile,ParentDirectory,flag + , CAST (null AS NVARCHAR(MAX)) AS container + , CAST([subdirectory] AS NVARCHAR(MAX)) AS dpath + FROM #DirectoryTree + WHERE ParentDirectory IS NULL + UNION ALL + SELECT + d.Id,d.subdirectory,d.depth,d.isfile,d.ParentDirectory,d.flag + , dpath as container + , dpath +'\'+d.[subdirectory] + FROM #DirectoryTree AS d + INNER JOIN dirs ON d.ParentDirectory = dirs.id + WHERE dpath NOT LIKE '%RECYCLE.BIN%' + ) + SELECT subdirectory as filename, container as filepath, isfile, dpath as fullpath FROM dirs + WHERE container IS NOT NULL + -- Dir style ordering + ORDER BY container, isfile, subdirectory" + + # build the query string based on how many directories they want to enumerate + $sql = $q1 + $sql += $($PathList | Where-Object { $_ -ne '' } | ForEach-Object { "$([System.Environment]::Newline)$($q2 -Replace 'dirname', $_)" }) + $sql += $query_files_sql + #Write-Message -Level Debug -Message $sql + return $sql + } + + function Format-Path { + param ($path) + $path = $path.Trim() + #Thank you windows 2000 + $path = $path -replace '[^A-Za-z0-9 _\.\-\\:]', '__' + return $path + } + + if ($FileType) { + $FileTypeComparison = $FileType | ForEach-Object { $_.ToLower() } | Where-Object { $_ } | Sort-Object | Get-Unique + } + } + + process { + foreach ($instance in $SqlInstance) { + + $paths = @() + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Get the default data and log directories from the instance + if (-not (Test-Bound -ParameterName Path)) { $Path = (Get-DbaDefaultPath -SqlInstance $server).Data } + + Write-Message -Level Verbose -Message "Adding paths" + $sql = Get-SQLDirTreeQuery $Path + Write-Message -Level Debug -Message $sql + + # This should remain as not .Query() to be compat with a PSProvider Chrissy is working on + $datatable = $server.ConnectionContext.ExecuteWithResults($sql).Tables.Rows + + Write-Message -Level Verbose -Message "$($datatable.Rows.Count) files found." + if ($FileTypeComparison) { + foreach ($row in $datatable) { + foreach ($type in $FileTypeComparison) { + if ($row.filename.ToLower().EndsWith(".$type")) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Filename = $row.fullpath + RemoteFilename = Join-AdminUnc -Servername $server.netname -Filepath $row.fullpath + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, RemoteFilename + } + } + } + } + else { + foreach ($row in $datatable) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Filename = $row.fullpath + RemoteFilename = Join-AdminUnc -Servername $server.netname -Filepath $row.fullpath + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, RemoteFilename + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Get-DbaForceNetworkEncryption { + <# + .SYNOPSIS + Gets Force Encryption settings for a SQL Server instance + + .DESCRIPTION + Gets Force Encryption settings for a SQL Server instance. Note that this requires access to the Windows Server - not the SQL instance itself. + + This setting is found in Configuration Manager. + + .PARAMETER SqlInstance + The target SQL Server - defaults to localhost. + + .PARAMETER Credential + Allows you to login to the computer (not sql instance) using alternative Windows credentials + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command + + .EXAMPLE + Get-DbaForceNetworkEncryption + + Gets Force Encryption properties on the default (MSSQLSERVER) instance on localhost - requires (and checks for) RunAs admin. + + .EXAMPLE + Get-DbaForceNetworkEncryption -SqlInstance sql01\SQL2008R2SP2 + + Gets Force Network Encryption for the SQL2008R2SP2 on sql01. Uses Windows Credentials to both login and view the registry. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [PSCredential] + + $Credential, + + [switch] + [Alias('Silent')]$EnableException + ) + process { + + foreach ($instance in $SqlInstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname" + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance" -Target $instance -Continue -Category InvalidArgument + } + + Write-Message -Level Verbose -Message "Connecting to SQL WMI on $($instance.ComputerName)" + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FullComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($($instance.InstanceName))" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + try { + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + } + catch { + # Probably because the instance name has been aliased or does not exist or samthin + } + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance" -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Verbose -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Verbose -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Verbose -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Verbose -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regpath = "Registry::HKEY_LOCAL_MACHINE\$($args[0])\MSSQLServer\SuperSocketNetLib" + $cert = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + $forceencryption = (Get-ItemProperty -Path $regpath -Name ForceEncryption).ForceEncryption + + # [pscustomobject] doesn't always work, unsure why. so return hashtable then turn it into pscustomobject on client + @{ + ComputerName = $env:COMPUTERNAME + InstanceName = $args[2] + SqlInstance = $args[1] + ForceEncryption = ($forceencryption -eq $true) + CertificateThumbprint = $cert + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $instance")) { + try { + $results = Invoke-Command2 -ComputerName $resolved.FullComputerName -Credential $Credential -ArgumentList $regroot, $vsname, $instancename -ScriptBlock $scriptblock -ErrorAction Stop -Raw + foreach ($result in $results) { + [pscustomobject]$result + } + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.FullComputerName) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} +function Get-DbaHelpIndex { + <# + .SYNOPSIS + Returns size, row and configuration information for indexes in databases. + + .DESCRIPTION + This function will return detailed information on indexes (and optionally statistics) for all indexes in a database, or a given index should one be passed along. + As this uses SQL Server DMVs to access the data it will only work in 2005 and up (sorry folks still running SQL Server 2000). + For performance reasons certain statistics information will not be returned from SQL Server 2005 if an ObjectName is not provided. + + The data includes: + - ObjectName: the table containing the index + - IndexType: clustered/non-clustered/columnstore and whether the index is unique/primary key + - KeyColumns: the key columns of the index + - IncludeColumns: any include columns in the index + - FilterDefinition: any filter that may have been used in the index + - DataCompression: row/page/none depending upon whether or not compression has been used + - IndexReads: the number of reads of the index since last restart or index rebuild + - IndexUpdates: the number of writes to the index since last restart or index rebuild + - SizeKB: the size the index in KB + - IndexRows: the number of the rows in the index (note filtered indexes will have fewer rows than exist in the table) + - IndexLookups: the number of lookups that have been performed (only applicable for the heap or clustered index) + - MostRecentlyUsed: when the index was most recently queried (default to 1900 for when never read) + - StatsSampleRows: the number of rows queried when the statistics were built/rebuilt (not included in SQL Server 2005 unless ObjectName is specified) + - StatsRowMods: the number of changes to the statistics since the last rebuild + - HistogramSteps: the number of steps in the statistics histogram (not included in SQL Server 2005 unless ObjectName is specified) + - StatsLastUpdated: when the statistics were last rebuilt (not included in SQL Server 2005 unless ObjectName is specified) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. This list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. This list is auto-populated from the server. + + .PARAMETER ObjectName + The name of a table for which you want to obtain the index information. If the two part naming convention for an object is not used it will use the default schema for the executing user. If not passed it will return data on all indexes in a given database. + + .PARAMETER IncludeStats + If this switch is enabled, statistics as well as indexes will be returned in the output (statistics information such as the StatsRowMods will always be returned for indexes). + + .PARAMETER IncludeDataTypes + If this switch is enabled, the output will include the data type of each column that makes up a part of the index definition (key and include columns). + + .PARAMETER Raw + If this switch is enabled, results may be less user-readable but more suitable for processing by other code. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Index + Author: Nic Cain, https://sirsql.net/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaHelpIndex + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB + + Returns information on all indexes on the MyDB database on the localhost. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB,MyDB2 + + Returns information on all indexes on the MyDB & MyDB2 databases. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB -ObjectName dbo.Table1 + + Returns index information on the object dbo.Table1 in the database MyDB. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB -ObjectName dbo.Table1 -IncludeStats + + Returns information on the indexes and statistics for the table dbo.Table1 in the MyDB database. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB -ObjectName dbo.Table1 -IncludeDataTypes + + Returns the index information for the table dbo.Table1 in the MyDB database, and includes the data types for the key and include columns. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB -ObjectName dbo.Table1 -Raw + + Returns the index information for the table dbo.Table1 in the MyDB database, and returns the numerical data without localized separators. + + .EXAMPLE + Get-DbaHelpIndex -SqlInstance localhost -Database MyDB -IncludeStats -Raw + + Returns the index information for all indexes in the MyDB database as well as their statistics, and formats the numerical data without localized separators. + #> + [CmdletBinding(SupportsShouldProcess = $false)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [string]$ObjectName, + [switch]$IncludeStats, + [switch]$IncludeDataTypes, + [switch]$Raw, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + #Add the table predicate to the query + if (!$ObjectName) { + $TablePredicate = "DECLARE @TableName NVARCHAR(256);"; + } + else { + $TablePredicate = "DECLARE @TableName NVARCHAR(256); SET @TableName = '$ObjectName';"; + } + + + #Figure out if we are including stats in the results + if ($IncludeStats) { + $IncludeStatsPredicate = ""; + } + else { + $IncludeStatsPredicate = "WHERE IndexType != 'STATISTICS'"; + } + + #Data types being returns with the results? + if ($IncludeDataTypes) { + $IncludeDataTypesPredicate = 'DECLARE @IncludeDataTypes BIT; SET @IncludeDataTypes = 1'; + } + else { + $IncludeDataTypesPredicate = 'DECLARE @IncludeDataTypes BIT; SET @IncludeDataTypes = 0'; + } + + #region SizesQuery + $SizesQuery = " + SET NOCOUNT ON; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + $TablePredicate + $IncludeDataTypesPredicate + ; + + DECLARE @IndexUsageStats TABLE + ( + object_id INT , + index_id INT , + user_scans BIGINT , + user_seeks BIGINT , + user_updates BIGINT , + user_lookups BIGINT , + last_user_lookup DATETIME2(0) , + last_user_scan DATETIME2(0) , + last_user_seek DATETIME2(0) + ); + + DECLARE @StatsInfo TABLE + ( + object_id INT , + stats_id INT , + stats_column_name NVARCHAR(128) , + stats_column_id INT , + stats_name NVARCHAR(128) , + stats_last_updated DATETIME2(0) , + stats_sampled_rows BIGINT , + rowmods BIGINT , + histogramsteps INT , + StatsRows BIGINT , + FullObjectName NVARCHAR(256) + ); + + INSERT INTO @IndexUsageStats + ( object_id , + index_id , + user_scans , + user_seeks , + user_updates , + user_lookups , + last_user_lookup , + last_user_scan , + last_user_seek + ) + SELECT object_id , + index_id , + user_scans , + user_seeks , + user_updates , + user_lookups , + last_user_lookup , + last_user_scan , + last_user_seek + FROM sys.dm_db_index_usage_stats + WHERE database_id = DB_ID(); + + INSERT INTO @StatsInfo + ( object_id , + stats_id , + stats_column_name , + stats_column_id , + stats_name , + stats_last_updated , + stats_sampled_rows , + rowmods , + histogramsteps , + StatsRows , + FullObjectName + ) + SELECT s.object_id , + s.stats_id , + c.name , + sc.stats_column_id , + s.name , + sp.last_updated , + sp.rows_sampled , + sp.modification_counter , + sp.steps , + sp.rows , + QUOTENAME(sch.name) + '.' + QUOTENAME(t.name) AS FullObjectName + FROM [sys].[stats] AS [s] + INNER JOIN sys.stats_columns sc ON s.stats_id = sc.stats_id + AND s.object_id = sc.object_id + INNER JOIN sys.columns c ON c.object_id = sc.object_id + AND c.column_id = sc.column_id + INNER JOIN sys.tables t ON c.object_id = t.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = t.schema_id + OUTER APPLY sys.dm_db_stats_properties([s].[object_id], + [s].[stats_id]) AS [sp] + WHERE s.object_id = CASE WHEN @TableName IS NULL THEN s.object_id + else OBJECT_ID(@TableName) + END; + + + ; + WITH cteStatsInfo + AS ( SELECT object_id , + si.stats_id , + si.stats_name , + STUFF((SELECT N', ' + stats_column_name + FROM @StatsInfo si2 + WHERE si2.object_id = si.object_id + AND si2.stats_id = si.stats_id + ORDER BY si2.stats_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', N'nvarchar(1000)'), 1, + 2, N'') AS StatsColumns , + MAX(si.stats_sampled_rows) AS SampleRows , + MAX(si.rowmods) AS RowMods , + MAX(si.histogramsteps) AS HistogramSteps , + MAX(si.stats_last_updated) AS StatsLastUpdated , + MAX(si.StatsRows) AS StatsRows, + FullObjectName + FROM @StatsInfo si + GROUP BY si.object_id , + si.stats_id , + si.stats_name , + si.FullObjectName + ), + cteIndexSizes + AS ( SELECT object_id , + index_id , + CASE WHEN index_id < 2 + THEN ( ( SUM(in_row_data_page_count + + lob_used_page_count + + row_overflow_used_page_count) + * 8192 ) / 1024 ) + else ( ( SUM(used_page_count) * 8192 ) / 1024 ) + END AS SizeKB + FROM sys.dm_db_partition_stats + GROUP BY object_id , + index_id + ), + cteRows + AS ( SELECT object_id , + index_id , + SUM(rows) AS IndexRows + FROM sys.partitions + GROUP BY object_id , + index_id + ), + cteIndex + AS ( SELECT OBJECT_NAME(c.object_id) AS ObjectName , + c.object_id , + c.index_id , + i.name COLLATE SQL_Latin1_General_CP1_CI_AS AS name , + c.index_column_id , + c.column_id , + c.is_included_column , + CASE WHEN @IncludeDataTypes = 0 + AND c.is_descending_key = 1 + THEN sc.name + ' DESC' + WHEN @IncludeDataTypes = 0 + AND c.is_descending_key = 0 THEN sc.name + WHEN @IncludeDataTypes = 1 + AND c.is_descending_key = 1 + AND c.is_included_column = 0 + THEN sc.name + ' DESC (' + t.name + ') ' + WHEN @IncludeDataTypes = 1 + AND c.is_descending_key = 0 + AND c.is_included_column = 0 + THEN sc.name + ' (' + t.name + ')' + else sc.name + END AS ColumnName , + i.filter_definition , + ISNULL(dd.user_scans, 0) AS user_scans , + ISNULL(dd.user_seeks, 0) AS user_seeks , + ISNULL(dd.user_updates, 0) AS user_updates , + ISNULL(dd.user_lookups, 0) AS user_lookups , + CONVERT(DATETIME2(0), ISNULL(dd.last_user_lookup, + '1901-01-01')) AS LastLookup , + CONVERT(DATETIME2(0), ISNULL(dd.last_user_scan, + '1901-01-01')) AS LastScan , + CONVERT(DATETIME2(0), ISNULL(dd.last_user_seek, + '1901-01-01')) AS LastSeek , + i.fill_factor , + c.is_descending_key , + p.data_compression_desc , + i.type_desc , + i.is_unique , + i.is_unique_constraint , + i.is_primary_key , + ci.SizeKB , + cr.IndexRows , + QUOTENAME(sch.name) + '.' + QUOTENAME(tbl.name) AS FullObjectName + FROM sys.indexes i + JOIN sys.index_columns c ON i.object_id = c.object_id + AND i.index_id = c.index_id + JOIN sys.columns sc ON c.object_id = sc.object_id + AND c.column_id = sc.column_id + INNER JOIN sys.tables tbl ON c.object_id = tbl.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = tbl.schema_id + LEFT JOIN sys.types t ON sc.user_type_id = t.user_type_id + LEFT JOIN @IndexUsageStats dd ON i.object_id = dd.object_id + AND i.index_id = dd.index_id --and dd.database_id = db_id() + JOIN sys.partitions p ON i.object_id = p.object_id + AND i.index_id = p.index_id + JOIN cteIndexSizes ci ON i.object_id = ci.object_id + AND i.index_id = ci.index_id + JOIN cteRows cr ON i.object_id = cr.object_id + AND i.index_id = cr.index_id + WHERE i.object_id = CASE WHEN @TableName IS NULL + THEN i.object_id + else OBJECT_ID(@TableName) + END + ), + cteResults + AS ( SELECT ci.FullObjectName , + ci.object_id , + MAX(index_id) AS Index_Id , + ci.type_desc + + CASE WHEN ci.is_primary_key = 1 + THEN ' (PRIMARY KEY)' + WHEN ci.is_unique_constraint = 1 + THEN ' (UNIQUE CONSTRAINT)' + WHEN ci.is_unique = 1 THEN ' (UNIQUE)' + else '' + END AS IndexType , + name AS IndexName , + STUFF((SELECT N', ' + ColumnName + FROM cteIndex ci2 + WHERE ci2.name = ci.name + AND ci2.is_included_column = 0 + GROUP BY ci2.index_column_id , + ci2.ColumnName + ORDER BY ci2.index_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', N'nvarchar(1000)'), 1, + 2, N'') AS KeyColumns , + ISNULL(STUFF((SELECT N', ' + ColumnName + FROM cteIndex ci3 + WHERE ci3.name = ci.name + AND ci3.is_included_column = 1 + GROUP BY ci3.index_column_id , + ci3.ColumnName + ORDER BY ci3.index_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', + N'nvarchar(1000)'), 1, 2, + N''), '') AS IncludeColumns , + ISNULL(filter_definition, '') AS FilterDefinition , + ci.fill_factor , + CASE WHEN ci.data_compression_desc = 'NONE' THEN '' + else ci.data_compression_desc + END AS DataCompression , + MAX(ci.user_seeks) + MAX(ci.user_scans) + + MAX(ci.user_lookups) AS IndexReads , + MAX(ci.user_lookups) AS IndexLookups , + ci.user_updates AS IndexUpdates , + ci.SizeKB AS SizeKB , + ci.IndexRows AS IndexRows , + CASE WHEN LastScan > LastSeek + AND LastScan > LastLookup THEN LastScan + WHEN LastSeek > LastScan + AND LastSeek > LastLookup THEN LastSeek + WHEN LastLookup > LastScan + AND LastLookup > LastSeek THEN LastLookup + else '' + END AS MostRecentlyUsed + FROM cteIndex ci + GROUP BY ci.ObjectName , + ci.name , + ci.filter_definition , + ci.object_id , + ci.LastLookup , + ci.LastSeek , + ci.LastScan , + ci.user_updates , + ci.fill_factor , + ci.data_compression_desc , + ci.type_desc , + ci.is_primary_key , + ci.is_unique , + ci.is_unique_constraint , + ci.SizeKB , + ci.IndexRows , + ci.FullObjectName + ), + AllResults + AS ( SELECT c.FullObjectName , + ISNULL(IndexType, 'STATISTICS') AS IndexType , + ISNULL(IndexName, si.stats_name) AS IndexName , + ISNULL(KeyColumns, si.StatsColumns) AS KeyColumns , + ISNULL(IncludeColumns, '') AS IncludeColumns , + FilterDefinition , + fill_factor AS [FillFactor] , + DataCompression , + IndexReads , + IndexUpdates , + SizeKB , + IndexRows , + IndexLookups , + MostRecentlyUsed , + SampleRows AS StatsSampleRows , + RowMods AS StatsRowMods , + si.HistogramSteps , + si.StatsLastUpdated , + 1 AS Ordering + FROM cteResults c + INNER JOIN cteStatsInfo si ON si.object_id = c.object_id + AND si.stats_id = c.Index_Id + UNION + SELECT QUOTENAME(sch.name) + '.' + QUOTENAME(tbl.name) AS FullObjectName , + 'STATISTICS' , + stats_name , + StatsColumns , + '' , + '' AS FilterDefinition , + '' AS Fill_Factor , + '' AS DataCompression , + '' AS IndexReads , + '' AS IndexUpdates , + '' AS SizeKB , + StatsRows AS IndexRows , + '' AS IndexLookups , + '' AS MostRecentlyUsed , + SampleRows AS StatsSampleRows , + RowMods AS StatsRowMods , + csi.HistogramSteps , + csi.StatsLastUpdated , + 2 + FROM cteStatsInfo csi + INNER JOIN sys.tables tbl ON csi.object_id = tbl.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = tbl.schema_id + WHERE stats_id NOT IN ( + SELECT stats_id + FROM cteResults c + INNER JOIN cteStatsInfo si ON si.object_id = c.object_id + AND si.stats_id = c.Index_Id ) + ) + SELECT FullObjectName , + ISNULL(IndexType, 'STATISTICS') AS IndexType , + IndexName , + KeyColumns , + ISNULL(IncludeColumns, '') AS IncludeColumns , + FilterDefinition , + [FillFactor] AS [FillFactor] , + DataCompression , + IndexReads , + IndexUpdates , + SizeKB , + IndexRows , + IndexLookups , + MostRecentlyUsed , + StatsSampleRows , + StatsRowMods , + HistogramSteps , + StatsLastUpdated + FROM AllResults + $IncludeStatsPredicate + OPTION ( RECOMPILE ); + " + #endRegion SizesQuery + + + #region sizesQuery2005 + $SizesQuery2005 = " + SET NOCOUNT ON; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + $TablePredicate + $IncludeDataTypesPredicate + ; + + DECLARE @AllResults TABLE + ( + RowNum INT , + FullObjectName NVARCHAR(300) , + IndexType NVARCHAR(256) , + IndexName NVARCHAR(256) , + KeyColumns NVARCHAR(2000) , + IncludeColumns NVARCHAR(2000) , + FilterDefinition NVARCHAR(100) , + [FillFactor] TINYINT , + DataCompression CHAR(4) , + IndexReads BIGINT , + IndexUpdates BIGINT , + SizeKB BIGINT , + IndexRows BIGINT , + IndexLookups BIGINT , + MostRecentlyUsed DATETIME , + StatsSampleRows BIGINT , + StatsRowMods BIGINT , + HistogramSteps INT , + StatsLastUpdated DATETIME , + object_id BIGINT , + index_id BIGINT + ); + + DECLARE @IndexUsageStats TABLE + ( + object_id INT , + index_id INT , + user_scans BIGINT , + user_seeks BIGINT , + user_updates BIGINT , + user_lookups BIGINT , + last_user_lookup DATETIME , + last_user_scan DATETIME , + last_user_seek DATETIME + ); + + DECLARE @StatsInfo TABLE + ( + object_id INT , + stats_id INT , + stats_column_name NVARCHAR(128) , + stats_column_id INT , + stats_name NVARCHAR(128) , + stats_last_updated DATETIME , + stats_sampled_rows BIGINT , + rowmods BIGINT , + histogramsteps INT , + StatsRows BIGINT , + FullObjectName NVARCHAR(256) + ); + + INSERT INTO @IndexUsageStats + ( object_id , + index_id , + user_scans , + user_seeks , + user_updates , + user_lookups , + last_user_lookup , + last_user_scan , + last_user_seek + ) + SELECT object_id , + index_id , + user_scans , + user_seeks , + user_updates , + user_lookups , + last_user_lookup , + last_user_scan , + last_user_seek + FROM sys.dm_db_index_usage_stats + WHERE database_id = DB_ID(); + + + INSERT INTO @StatsInfo + ( object_id , + stats_id , + stats_column_name , + stats_column_id , + stats_name , + stats_last_updated , + stats_sampled_rows , + rowmods , + histogramsteps , + StatsRows , + FullObjectName + ) + SELECT s.object_id , + s.stats_id , + c.name , + sc.stats_column_id , + s.name , + NULL AS last_updated , + NULL AS rows_sampled , + NULL AS modification_counter , + NULL AS steps , + NULL AS rows , + QUOTENAME(sch.name) + '.' + QUOTENAME(t.name) AS FullObjectName + FROM [sys].[stats] AS [s] + INNER JOIN sys.stats_columns sc ON s.stats_id = sc.stats_id + AND s.object_id = sc.object_id + INNER JOIN sys.columns c ON c.object_id = sc.object_id + AND c.column_id = sc.column_id + INNER JOIN sys.tables t ON c.object_id = t.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = t.schema_id + -- OUTER APPLY sys.dm_db_stats_properties([s].[object_id], + -- [s].[stats_id]) AS [sp] + WHERE s.object_id = CASE WHEN @TableName IS NULL THEN s.object_id + else OBJECT_ID(@TableName) + END; + + + ; + WITH cteStatsInfo + AS ( SELECT object_id , + si.stats_id , + si.stats_name , + STUFF((SELECT N', ' + stats_column_name + FROM @StatsInfo si2 + WHERE si2.object_id = si.object_id + AND si2.stats_id = si.stats_id + ORDER BY si2.stats_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', N'nvarchar(1000)'), 1, + 2, N'') AS StatsColumns , + MAX(si.stats_sampled_rows) AS SampleRows , + MAX(si.rowmods) AS RowMods , + MAX(si.histogramsteps) AS HistogramSteps , + MAX(si.stats_last_updated) AS StatsLastUpdated , + MAX(si.StatsRows) AS StatsRows, + FullObjectName + FROM @StatsInfo si + GROUP BY si.object_id , + si.stats_id , + si.stats_name , + si.FullObjectName + ), + cteIndexSizes + AS ( SELECT object_id , + index_id , + CASE WHEN index_id < 2 + THEN ( ( SUM(in_row_data_page_count + + lob_used_page_count + + row_overflow_used_page_count) + * 8192 ) / 1024 ) + else ( ( SUM(used_page_count) * 8192 ) / 1024 ) + END AS SizeKB + FROM sys.dm_db_partition_stats + GROUP BY object_id , + index_id + ), + cteRows + AS ( SELECT object_id , + index_id , + SUM(rows) AS IndexRows + FROM sys.partitions + GROUP BY object_id , + index_id + ), + cteIndex + AS ( SELECT OBJECT_NAME(c.object_id) AS ObjectName , + c.object_id , + c.index_id , + i.name COLLATE SQL_Latin1_General_CP1_CI_AS AS name , + c.index_column_id , + c.column_id , + c.is_included_column , + CASE WHEN @IncludeDataTypes = 0 + AND c.is_descending_key = 1 + THEN sc.name + ' DESC' + WHEN @IncludeDataTypes = 0 + AND c.is_descending_key = 0 THEN sc.name + WHEN @IncludeDataTypes = 1 + AND c.is_descending_key = 1 + AND c.is_included_column = 0 + THEN sc.name + ' DESC (' + t.name + ') ' + WHEN @IncludeDataTypes = 1 + AND c.is_descending_key = 0 + AND c.is_included_column = 0 + THEN sc.name + ' (' + t.name + ')' + else sc.name + END AS ColumnName , + '' AS filter_definition , + ISNULL(dd.user_scans, 0) AS user_scans , + ISNULL(dd.user_seeks, 0) AS user_seeks , + ISNULL(dd.user_updates, 0) AS user_updates , + ISNULL(dd.user_lookups, 0) AS user_lookups , + CONVERT(DATETIME, ISNULL(dd.last_user_lookup, + '1901-01-01')) AS LastLookup , + CONVERT(DATETIME, ISNULL(dd.last_user_scan, + '1901-01-01')) AS LastScan , + CONVERT(DATETIME, ISNULL(dd.last_user_seek, + '1901-01-01')) AS LastSeek , + i.fill_factor , + c.is_descending_key , + 'NONE' as data_compression_desc , + i.type_desc , + i.is_unique , + i.is_unique_constraint , + i.is_primary_key , + ci.SizeKB , + cr.IndexRows , + QUOTENAME(sch.name) + '.' + QUOTENAME(tbl.name) AS FullObjectName + FROM sys.indexes i + JOIN sys.index_columns c ON i.object_id = c.object_id + AND i.index_id = c.index_id + JOIN sys.columns sc ON c.object_id = sc.object_id + AND c.column_id = sc.column_id + INNER JOIN sys.tables tbl ON c.object_id = tbl.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = tbl.schema_id + LEFT JOIN sys.types t ON sc.user_type_id = t.user_type_id + LEFT JOIN @IndexUsageStats dd ON i.object_id = dd.object_id + AND i.index_id = dd.index_id --and dd.database_id = db_id() + JOIN sys.partitions p ON i.object_id = p.object_id + AND i.index_id = p.index_id + JOIN cteIndexSizes ci ON i.object_id = ci.object_id + AND i.index_id = ci.index_id + JOIN cteRows cr ON i.object_id = cr.object_id + AND i.index_id = cr.index_id + WHERE i.object_id = CASE WHEN @TableName IS NULL + THEN i.object_id + else OBJECT_ID(@TableName) + END + ), + cteResults + AS ( SELECT ci.FullObjectName , + ci.object_id , + MAX(index_id) AS Index_Id , + ci.type_desc + + CASE WHEN ci.is_primary_key = 1 + THEN ' (PRIMARY KEY)' + WHEN ci.is_unique_constraint = 1 + THEN ' (UNIQUE CONSTRAINT)' + WHEN ci.is_unique = 1 THEN ' (UNIQUE)' + else '' + END AS IndexType , + name AS IndexName , + STUFF((SELECT N', ' + ColumnName + FROM cteIndex ci2 + WHERE ci2.name = ci.name + AND ci2.is_included_column = 0 + GROUP BY ci2.index_column_id , + ci2.ColumnName + ORDER BY ci2.index_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', N'nvarchar(1000)'), 1, + 2, N'') AS KeyColumns , + ISNULL(STUFF((SELECT N', ' + ColumnName + FROM cteIndex ci3 + WHERE ci3.name = ci.name + AND ci3.is_included_column = 1 + GROUP BY ci3.index_column_id , + ci3.ColumnName + ORDER BY ci3.index_column_id + FOR XML PATH(N'') , + TYPE).value(N'.[1]', + N'nvarchar(1000)'), 1, 2, + N''), '') AS IncludeColumns , + ISNULL(filter_definition, '') AS FilterDefinition , + ci.fill_factor , + CASE WHEN ci.data_compression_desc = 'NONE' THEN '' + else ci.data_compression_desc + END AS DataCompression , + MAX(ci.user_seeks) + MAX(ci.user_scans) + + MAX(ci.user_lookups) AS IndexReads , + MAX(ci.user_lookups) AS IndexLookups , + ci.user_updates AS IndexUpdates , + ci.SizeKB AS SizeKB , + ci.IndexRows AS IndexRows , + CASE WHEN LastScan > LastSeek + AND LastScan > LastLookup THEN LastScan + WHEN LastSeek > LastScan + AND LastSeek > LastLookup THEN LastSeek + WHEN LastLookup > LastScan + AND LastLookup > LastSeek THEN LastLookup + else '' + END AS MostRecentlyUsed + FROM cteIndex ci + GROUP BY ci.ObjectName , + ci.name , + ci.filter_definition , + ci.object_id , + ci.LastLookup , + ci.LastSeek , + ci.LastScan , + ci.user_updates , + ci.fill_factor , + ci.data_compression_desc , + ci.type_desc , + ci.is_primary_key , + ci.is_unique , + ci.is_unique_constraint , + ci.SizeKB , + ci.IndexRows , + ci.FullObjectName + ), AllResults AS + ( SELECT c.FullObjectName , + ISNULL(IndexType, 'STATISTICS') AS IndexType , + ISNULL(IndexName, '') AS IndexName , + ISNULL(KeyColumns, '') AS KeyColumns , + ISNULL(IncludeColumns, '') AS IncludeColumns , + FilterDefinition , + fill_factor AS [FillFactor] , + DataCompression , + IndexReads , + IndexUpdates , + SizeKB , + IndexRows , + IndexLookups , + MostRecentlyUsed , + NULL AS StatsSampleRows , + NULL AS StatsRowMods , + NULL AS HistogramSteps , + NULL AS StatsLastUpdated , + 1 AS Ordering , + c.object_id , + c.Index_Id + FROM cteResults c + INNER JOIN cteStatsInfo si ON si.object_id = c.object_id + AND si.stats_id = c.Index_Id + UNION + SELECT QUOTENAME(sch.name) + '.' + QUOTENAME(tbl.name) AS FullObjectName , + 'STATISTICS' , + stats_name , + StatsColumns , + '' , + '' AS FilterDefinition , + '' AS Fill_Factor , + '' AS DataCompression , + '' AS IndexReads , + '' AS IndexUpdates , + '' AS SizeKB , + StatsRows AS IndexRows , + '' AS IndexLookups , + '' AS MostRecentlyUsed , + SampleRows AS StatsSampleRows , + RowMods AS StatsRowMods , + csi.HistogramSteps , + csi.StatsLastUpdated , + 2 , + csi.object_id , + csi.stats_id + FROM cteStatsInfo csi + INNER JOIN sys.tables tbl ON csi.object_id = tbl.object_id + INNER JOIN sys.schemas sch ON sch.schema_id = tbl.schema_id + LEFT JOIN (SELECT si.object_id, si.stats_id + FROM cteResults c + INNER JOIN cteStatsInfo si ON si.object_id = c.object_id + AND si.stats_id = c.Index_Id ) AS x on csi.object_id = x.object_id and csi.stats_id = x.stats_id + WHERE x.object_id is null + ) + INSERT INTO @AllResults + SELECT row_number() OVER (ORDER BY FullObjectName) AS RowNum , + FullObjectName , + ISNULL(IndexType, 'STATISTICS') AS IndexType , + IndexName , + KeyColumns , + ISNULL(IncludeColumns, '') AS IncludeColumns , + FilterDefinition , + [FillFactor] AS [FillFactor] , + DataCompression , + IndexReads , + IndexUpdates , + SizeKB , + IndexRows , + IndexLookups , + MostRecentlyUsed , + StatsSampleRows , + StatsRowMods , + HistogramSteps , + StatsLastUpdated , + object_id , + index_id + FROM AllResults + $IncludeStatsPredicate + OPTION ( RECOMPILE ); + + /* Only update the stats data on 2005 for a single table, otherwise the run time for this is a potential problem for large table/index volumes */ + if @TableName IS NOT NULL + BEGIN + + DECLARE @StatsInfo2005 TABLE (Name nvarchar(128), Updated DATETIME, Rows BIGINT, RowsSampled BIGINT, Steps INT, Density INT, AverageKeyLength INT, StringIndex NVARCHAR(20)) + + DECLARE @SqlCall NVARCHAR(2000), @RowNum INT; + SELECT @RowNum = min(RowNum) FROM @AllResults; + WHILE @RowNum IS NOT NULL + BEGIN + SELECT @SqlCall = 'dbcc show_statistics('+FullObjectName+', '+IndexName+') with stat_header' FROM @AllResults WHERE RowNum = @RowNum; + INSERT INTO @StatsInfo2005 exec (@SqlCall); + UPDATE @AllResults + SET StatsSampleRows = RowsSampled, + HistogramSteps = Steps, + StatsLastUpdated = Updated + FROM @StatsInfo2005 + WHERE RowNum = @RowNum; + DELETE FROM @StatsInfo2005 + SELECT @RowNum = min(RowNum) FROM @AllResults WHERE RowNum > @RowNum; + END; + + END; + + UPDATE a + SET a.StatsRowMods = i.rowmodctr + FROM @AllResults a + JOIN sys.sysindexes i ON a.object_id = i.id AND a.index_id = i.indid; + + SELECT FullObjectName , + IndexType , + IndexName , + KeyColumns , + IncludeColumns , + FilterDefinition , + [FillFactor] , + DataCompression , + IndexReads , + IndexUpdates , + SizeKB , + IndexRows , + IndexLookups , + MostRecentlyUsed , + StatsSampleRows , + StatsRowMods , + HistogramSteps , + StatsLastUpdated + FROM @AllResults;" + + #endregion sizesQuery2005 + $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential + } + process { + Write-Message -Level Debug -Message $SizesQuery + Write-Message -Level Debug -Message $SizesQuery2005 + #Need to check the version of SQL + if ($server.versionMajor -ge 10) { + $indexesQuery = $SizesQuery + } + + elseif ($server.Information.Version.Major -eq 9) { + $indexesQuery = $SizesQuery2005 + } + + else { + Write-Warning "This function does not support versions lower than SQL Server 2005 (v9)." + continue + } + + if ($pipedatabase.Length -gt 0) { + $databases = $pipedatabase.name + } + + $databases = $server.Databases + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "$db is not accessible. Skipping." + continue + } + try { + $IndexDetails = ($server.Databases[$db.Name].ExecuteWithResults($indexesQuery)).Tables[0]; + + if (!$Raw) { + foreach ($detail in $IndexDetails) { + $recentlyused = [datetime]$detail.MostRecentlyUsed + + if ($recentlyused.year -eq 1900) { + $recentlyused = $null + } + + [pscustomobject]@{ + DatabaseName = $db.Name + ObjectName = $detail.FullObjectName + IndexName = $detail.IndexName + IndexType = $detail.IndexType + KeyColumns = $detail.KeyColumns + IncludeColumns = $detail.IncludeColumns + FilterDefinition = $detail.FilterDefinition + DataCompression = $detail.DataCompression + IndexReads = "{0:N0}" -f $detail.IndexReads + IndexUpdates = "{0:N0}" -f $detail.IndexUpdates + SizeKB = "{0:N0}" -f $detail.SizeKB + IndexRows = "{0:N0}" -f $detail.IndexRows + IndexLookups = "{0:N0}" -f $detail.IndexLookups + MostRecentlyUsed = $recentlyused + StatsSampleRows = "{0:N0}" -f $detail.StatsSampleRows + StatsRowMods = "{0:N0}" -f $detail.StatsRowMods + HistogramSteps = $detail.HistogramSteps + StatsLastUpdated = $detail.StatsLastUpdated + } + } + } + + else { + foreach ($detail in $IndexDetails) { + $recentlyused = [datetime]$detail.MostRecentlyUsed + + if ($recentlyused.year -eq 1900) { + $recentlyused = $null + } + + [pscustomobject]@{ + DatabaseName = $db.Name + ObjectName = $detail.FullObjectName + IndexName = $detail.IndexName + IndexType = $detail.IndexType + KeyColumns = $detail.KeyColumns + IncludeColumns = $detail.IncludeColumns + FilterDefinition = $detail.FilterDefinition + DataCompression = $detail.DataCompression + IndexReads = $detail.IndexReads + IndexUpdates = $detail.IndexUpdates + SizeKB = $detail.SizeKB + IndexRows = $detail.IndexRows + IndexLookups = $detail.IndexLookups + MostRecentlyUsed = $recentlyused + StatsSampleRows = $detail.StatsSampleRows + StatsRowMods = $detail.StatsRowMods + HistogramSteps = $detail.HistogramSteps + StatsLastUpdated = $detail.StatsLastUpdated + } + } + } + } + catch { + Write-Warning "Cannot process $db on $server." + } + } + } +} + +#ValidationTags#Messaging,FlowControl,CodeStyle# +function Get-DbaJobCategory { + <# + .SYNOPSIS + Gets SQL Agent Job Category information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaJobCategory returns connected SMO object for SQL Agent Job Category information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, Category + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaJobCategory + + .EXAMPLE + Get-DbaJobCategory -SqlInstance localhost + + Returns all SQL Agent Job Categories on the local default SQL Server instance + + .EXAMPLE + Get-DbaJobCategory -SqlInstance localhost, sql2016 + + Returns all SQL Agent Job Categories for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($jobCategory in $server.JobServer.JobCategories) { + Add-Member -Force -InputObject $jobCategory -MemberType NoteProperty -Name ComputerName -value $jobCategory.Parent.Parent.NetName + Add-Member -Force -InputObject $jobCategory -MemberType NoteProperty -Name InstanceName -value $jobCategory.Parent.Parent.ServiceName + Add-Member -Force -InputObject $jobCategory -MemberType NoteProperty -Name SqlInstance -value $jobCategory.Parent.Parent.DomainInstanceName + + Select-DefaultView -InputObject $jobCategory -Property ComputerName, InstanceName, SqlInstance, ID, Name, CategoryType + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaLastBackup { + <# + .SYNOPSIS + Get date/time for last known backups of databases. + + .DESCRIPTION + Retrieves and compares the date/time for the last known backups, as well as the creation date/time for the database. + + Default output includes columns Server, Database, RecoveryModel, LastFullBackup, LastDiffBackup, LastLogBackup, SinceFull, SinceDiff, SinceLog, Status, DatabaseCreated, DaysSinceDbCreated. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER EnableException + If this switch is enabled exceptions will be thrown to the caller, which will need to perform its own exception processing. Otherwise, the function will try to catch the exception, interpret it and provide a friendly error message. + + .NOTES + Tags: DisasterRecovery, Backup + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaLastBackup + + .EXAMPLE + Get-DbaLastBackup -SqlInstance ServerA\sql987 + + Returns a custom object displaying Server, Database, RecoveryModel, LastFullBackup, LastDiffBackup, LastLogBackup, SinceFull, SinceDiff, SinceLog, Status, DatabaseCreated, DaysSinceDbCreated + + .EXAMPLE + Get-DbaLastBackup -SqlInstance ServerA\sql987 + + Returns a custom object with Server name, Database name, and the date the last time backups were performed. + + .EXAMPLE + Get-DbaLastBackup -SqlInstance ServerA\sql987 | Select * + + Returns a custom object with Server name, Database name, and the date the last time backups were performed, and also recoverymodel and calculations on how long ago backups were taken and what the status is. + + .EXAMPLE + Get-DbaLastBackup -SqlInstance ServerA\sql987 | Select * | Out-Gridview + + Returns a gridview displaying Server, Database, RecoveryModel, LastFullBackup, LastDiffBackup, LastLogBackup, SinceFull, SinceDiff, SinceLog, Status, DatabaseCreated, DaysSinceDbCreated. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function Get-DbaDateOrNull ($TimeSpan) { + if ($TimeSpan -eq 0) { + return $null + } + return $TimeSpan + } + $StartOfTime = [DbaTimeSpan](New-TimeSpan -Start ([datetime]0)) + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Message -Level Warning -Message "Can't connect to $instance" + Continue + } + + $dbs = $server.Databases | Where-Object { $_.name -ne 'tempdb' } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + # Get-DbaBackupHistory -Last would make the job in one query but SMO's (and this) report the last backup of this type irregardless of the chain + $FullHistory = Get-DbaBackupHistory -SqlInstance $server -Database $dbs.Name -LastFull -IncludeCopyOnly -Raw + $DiffHistory = Get-DbaBackupHistory -SqlInstance $server -Database $dbs.Name -LastDiff -IncludeCopyOnly -Raw + $IncrHistory = Get-DbaBackupHistory -SqlInstance $server -Database $dbs.Name -LastLog -IncludeCopyOnly -Raw + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "The database $db on server $instance is not accessible. Skipping database." + Continue + } + $LastFullBackup = ($FullHistory | Where-Object Database -eq $db.Name | Sort-Object -Property End -Descending | Select-Object -First 1).End + if ($null -ne $LastFullBackup) { + $SinceFull_ = [DbaTimeSpan](New-TimeSpan -Start $LastFullBackup) + } + else { + $SinceFull_ = $StartOfTime + } + + $LastDiffBackup = ($DiffHistory | Where-Object Database -eq $db.Name | Sort-Object -Property End -Descending | Select-Object -First 1).End + if ($null -ne $LastDiffBackup) { + $SinceDiff_ = [DbaTimeSpan](New-TimeSpan -Start $LastDiffBackup) + } + else { + $SinceDiff_ = $StartOfTime + } + + $LastIncrBackup = ($IncrHistory | Where-Object Database -eq $db.Name | Sort-Object -Property End -Descending | Select-Object -First 1).End + if ($null -ne $LastIncrBackup) { + $SinceLog_ = [DbaTimeSpan](New-TimeSpan -Start $LastIncrBackup) + } + else { + $SinceLog_ = $StartOfTime + } + + $daysSinceDbCreated = (New-TimeSpan -Start $db.createDate).Days + + if ($daysSinceDbCreated -lt 1 -and $SinceFull_ -eq 0) { + $Status = 'New database, not backed up yet' + } + elseif ($SinceFull_.Days -gt 0 -and $SinceDiff_.Days -gt 0) { + $Status = 'No Full or Diff Back Up in the last day' + } + elseif ($db.RecoveryModel -eq "Full" -and $SinceLog_.Hours -gt 0) { + $Status = 'No Log Back Up in the last hour' + } + else { + $Status = 'OK' + } + + $result = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + RecoveryModel = $db.RecoveryModel + LastFullBackup = [DbaDateTime]$LastFullBackup + LastDiffBackup = [DbaDateTime]$LastDiffBackup + LastLogBackup = [DbaDateTime]$LastIncrBackup + SinceFull = Get-DbaDateOrNull -TimeSpan $SinceFull_ + SinceDiff = Get-DbaDateOrNull -TimeSpan $SinceDiff_ + SinceLog = Get-DbaDateOrNull -TimeSpan $SinceLog_ + DatabaseCreated = $db.createDate + DaysSinceDbCreated = $daysSinceDbCreated + Status = $status + } + Select-DefaultView -InputObject $result -Property ComputerName, InstanceName, SqlInstance, Database, LastFullBackup, LastDiffBackup, LastLogBackup + } + } + } +} +function Get-DbaLastGoodCheckDb { + <# + .SYNOPSIS + Get date/time for last known good DBCC CHECKDB + + .DESCRIPTION + Retrieves and compares the date/time for the last known good DBCC CHECKDB, as well as the creation date/time for the database. + + This function supports SQL Server 2005 and higher. + + Please note that this script uses the DBCC DBINFO() WITH TABLERESULTS. DBCC DBINFO has several known weak points, such as: + - DBCC DBINFO is an undocumented feature/command. + - The LastKnowGood timestamp is updated when a DBCC CHECKFILEGROUP is performed. + - The LastKnowGood timestamp is updated when a DBCC CHECKDB WITH PHYSICAL_ONLY is performed. + - The LastKnowGood timestamp does not get updated when a database in READ_ONLY. + + An empty ($null) LastGoodCheckDb result indicates that a good DBCC CHECKDB has never been performed. + + SQL Server 2008R2 has a "bug" that causes each databases to possess two dbi_dbccLastKnownGood fields, instead of the normal one. + + This script will only display this function to only display the newest timestamp. If -Verbose is specified, the function will announce every time more than one dbi_dbccLastKnownGood fields is encountered. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CHECKDB, Database + Author: Jakob Bindslet (jakob@bindslet.dk) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + DBCC CHECKDB: + https://msdn.microsoft.com/en-us/library/ms176064.aspx + http://www.sqlcopilot.com/dbcc-checkdb.html + Data Purity: + http://www.sqlskills.com/blogs/paul/checkdb-from-every-angle-how-to-tell-if-data-purity-checks-will-be-run/ + https://www.mssqltips.com/sqlservertip/1988/ensure-sql-server-data-purity-checks-are-performed/ + + .EXAMPLE + Get-DbaLastGoodCheckDb -SqlInstance ServerA\sql987 + + Returns a custom object displaying Server, Database, DatabaseCreated, LastGoodCheckDb, DaysSinceDbCreated, DaysSinceLastGoodCheckDb, Status and DataPurityEnabled + + .EXAMPLE + Get-DbaLastGoodCheckDb -SqlInstance ServerA\sql987 -SqlCredential (Get-Credential sqladmin) | Format-Table -AutoSize + + Returns a formatted table displaying Server, Database, DatabaseCreated, LastGoodCheckDb, DaysSinceDbCreated, DaysSinceLastGoodCheckDb, Status and DataPurityEnabled. Authenticates using SQL Server authentication. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.versionMajor -lt 9) { + Stop-Function -Message "Get-DbaLastGoodCheckDb is only supported on SQL Server 2005 and above. Skipping Instance." -Continue -Target $instance + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instances." + + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "The database $db is not accessible. Skipping database." -Continue -Target $db + } + + $sql = "DBCC DBINFO ([$($db.name)]) WITH TABLERESULTS" + Write-Message -Level Debug -Message "T-SQL: $sql" + + $resultTable = $db.ExecuteWithResults($sql).Tables[0] + [datetime[]]$lastKnownGoodArray = $resultTable | Where-Object Field -eq 'dbi_dbccLastKnownGood' | Select-Object -ExpandProperty Value + + ## look for databases with two or more occurrences of the field dbi_dbccLastKnownGood + if ($lastKnownGoodArray.count -ge 2) { + Write-Message -Level Verbose -Message "The database $db has $($lastKnownGoodArray.count) dbi_dbccLastKnownGood fields. This script will only use the newest!" + } + [datetime]$lastKnownGood = $lastKnownGoodArray | Sort-Object -Descending | Select-Object -First 1 + + [int]$createVersion = ($resultTable | Where-Object Field -eq 'dbi_createVersion').Value + [int]$dbccFlags = ($resultTable | Where-Object Field -eq 'dbi_dbccFlags').Value + + if (($createVersion -lt 611) -and ($dbccFlags -eq 0)) { + $dataPurityEnabled = $false + } + else { + $dataPurityEnabled = $true + } + + $daysSinceCheckDb = (New-TimeSpan -Start $lastKnownGood -End (Get-Date)).Days + $daysSinceDbCreated = (New-TimeSpan -Start $db.createDate -End (Get-Date)).TotalDays + + if ($daysSinceCheckDb -lt 7) { + $Status = 'Ok' + } + elseif ($daysSinceDbCreated -lt 7) { + $Status = 'New database, not checked yet' + } + else { + $Status = 'CheckDB should be performed' + } + + if ($lastKnownGood -eq '1/1/1900 12:00:00 AM') { + Remove-Variable -Name lastKnownGood, daysSinceCheckDb + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + DatabaseCreated = $db.createDate + LastGoodCheckDb = $lastKnownGood + DaysSinceDbCreated = $daysSinceDbCreated + DaysSinceLastGoodCheckDb = $daysSinceCheckDb + Status = $status + DataPurityEnabled = $dataPurityEnabled + CreateVersion = $createVersion + DbccFlags = $dbccFlags + } + } + } + } +} +function Get-DbaLinkedServer { + <# + .SYNOPSIS + Gets all linked servers and summary of information from the sql servers listed + + .DESCRIPTION + Retrieves information about each linked server on the instance + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER LinkedServer + The linked server(s) to process - this list is auto-populated from the server. If unspecified, all linked servers will be processed. + + .PARAMETER ExcludeLinkedServer + The linked server(s) to exclude - this list is auto-populated from the server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: LinkedServer, Linked + Author: Stephen Bennett ( https://sqlnotesfromtheunderground.wordpress.com/ ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaLinkedServer + + .EXAMPLE + Get-DbaLinkedServer -SqlInstance DEV01 + + Returns all Linked Servers for the SQL Server instance DEV01 + #> + [CmdletBinding(DefaultParameterSetName = 'Default')] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$LinkedServer, + [object[]]$ExcludeLinkedServer, + [Alias('Silent')] + [switch]$EnableException + ) + foreach ($Instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $lservers = $server.LinkedServers + + if ($LinkedServer) { + $lservers = $lservers | Where-Object { $_.Name -in $LinkedServer } + } + if ($ExcludeLinkedServer) { + $lservers = $lservers | Where-Object { $_.Name -notin $ExcludeLinkedServer } + } + + foreach ($ls in $lservers) { + Add-Member -Force -InputObject $ls -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $ls -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $ls -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $ls -MemberType NoteProperty -Name Impersonate -value $ls.LinkedServerLogins.Impersonate + Add-Member -Force -InputObject $ls -MemberType NoteProperty -Name RemoteUser -value $ls.LinkedServerLogins.RemoteUser + + Select-DefaultView -InputObject $ls -Property ComputerName, InstanceName, SqlInstance, Name, 'DataSource as RemoteServer', ProductName, Impersonate, RemoteUser, 'DistPublisher as Publisher', Distributor, DateLastModified + } + } +} +function Get-DbaLocaleSetting { + <# + .SYNOPSIS + Gets the Locale settings on a computer. + + .DESCRIPTION + Gets the Locale settings on one or more computers. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: OS + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaLocaleSetting + + .EXAMPLE + Get-DbaLocaleSetting -ComputerName sqlserver2014a + + Gets the Locale settings on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaLocaleSetting + + Gets the Locale settings on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaLocaleSetting -ComputerName sql1,sql2 | Out-Gridview + + Gets the Locale settings on computers sql1 and sql2, and shows them in a grid view. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [string[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique + $sessionoption = New-CimSessionOption -Protocol DCom + $keyname = "Control Panel\International" + $NS = 'root\cimv2' + $Reg = 'StdRegProv' + [UInt32]$CIMHiveCU = 2147483649 + } + PROCESS { + foreach ($computer in $ComputerName) { + $props = @{ "ComputerName" = $computer } + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ( $Server.FullComputerName ) { + $Computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan" + $CIMsession = New-CimSession -ComputerName $Computer -ErrorAction SilentlyContinue -Credential $Credential + if ( -not $CIMSession ) { + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan failed. Creating CIMSession on $computer over DCom" + $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential + } + if ( $CIMSession ) { + Write-Message -Level Verbose -Message "Getting properties from Registry Key" + $PropNames = Invoke-CimMethod -CimSession $CIMsession -Namespace $NS -ClassName $Reg -MethodName enumvalues -Arguments @{hDefKey = $CIMHiveCU; sSubKeyName = $keyname} | + Select-Object -ExpandProperty snames + + foreach ($Name in $PropNames) { + $sValue = Invoke-CimMethod -CimSession $CIMsession -Namespace $NS -ClassName $Reg -MethodName GetSTRINGvalue -Arguments @{hDefKey = $CIMHiveCU; sSubKeyName = $keyname; sValueName = $Name} | + Select-Object -ExpandProperty svalue + $props.add($Name, $sValue) + } + [PSCustomObject]$props + } #if CIMSession + else { + Write-Message -Level Warning -Message "Can't create CIMSession on $computer" + } + } #if computername + else { + Write-Message -Level Warning -Message "Can't connect to $computer" + } + } #foreach computer + } #PROCESS +} #function +function Get-DbaLogin { + <# + .SYNOPSIS + Function to get an SMO login object of the logins for a given SQL Instance. Takes a server object from the pipe + + .DESCRIPTION + The Get-DbaLogin function returns an SMO Login object for the logins passed, if there are no users passed it will return all logins. + + .PARAMETER SqlInstance + The SQL Server instance, or instances.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER Login + The login(s) to process - this list is auto-populated from the server. If unspecified, all logins will be processed. + + .PARAMETER ExcludeLogin + The login(s) to exclude - this list is auto-populated from the server + + .PARAMETER IncludeFilter + A list of logins to include - accepts wildcard patterns + + .PARAMETER ExcludeFilter + A list of logins to exclude - accepts wildcard patterns + + .PARAMETER NoSystem + A Switch to remove System Logins from the output. + + .PARAMETER SQLLogins + A Switch to return Logins of type SQLLogin only. + + .PARAMETER WindowsLogins + A Switch to return Logins of type Windows only. + + .PARAMETER Locked + A Switch to return locked Logins. + + .PARAMETER Disabled + A Switch to return disabled Logins. + + .PARAMETER HasAccess + A Switch to return Logins that have access to the instance of SQL Server. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Login, Security + Author: Mitchell Hamann (@SirCaptainMitch) + Author: Klaas Vandenberghe (@powerdbaklaas) + Author: Robert Corrigan (@rjcorrig) + Author: Rob Sewell (@SQLDBaWithBeard) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaLogin + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 + + Gets all the logins from server sql2016 using NT authentication and returns the SMO login objects + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -SqlCredential $sqlcred + + Gets all the logins for a given SQL Server using a passed credential object and returns the SMO login objects + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -SqlCredential $sqlcred -Login dbatoolsuser,TheCaptain + + Get specific logins from server sql2016 returned as SMO login objects. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -IncludeFilter '##*','NT *' + + Get all user objects from server sql2016 beginning with '##' or 'NT ', returned as SMO login objects. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -ExcludeLogin dbatoolsuser + + Get all user objects from server sql2016 except the login dbatoolsuser, returned as SMO login objects. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -WindowsLogins + + Get all user objects from server sql2016 that are Windows Logins + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -WindowsLogins -IncludeFilter *Rob* + + Get all user objects from server sql2016 that are Windows Logins and have Rob in the name + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -SQLLogins + + Get all user objects from server sql2016 that are SQLLogins + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -SQLLogins -IncludeFilter *Rob* + + Get all user objects from server sql2016 that are SQLLogins and have Rob in the name + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -NoSystem + + Get all user objects from server sql2016 that are not system objects + + .EXAMPLE + Get-DbaLogin -SqlInstance sql2016 -ExcludeFilter '##*','NT *' + + Get all user objects from server sql2016 except any beginning with '##' or 'NT ', returned as SMO login objects. + + .EXAMPLE + 'sql2016', 'sql2014' | Get-DbaLogin -SqlCredential $sqlcred + + Using Get-DbaLogin on the pipeline, you can also specify which names you would like with -Login. + + .EXAMPLE + 'sql2016', 'sql2014' | Get-DbaLogin -SqlCredential $sqlcred -Locked + + Using Get-DbaLogin on the pipeline to get all locked logins on servers sql2016 and sql2014. + + .EXAMPLE + 'sql2016', 'sql2014' | Get-DbaLogin -SqlCredential $sqlcred -HasAccess -Disabled + + Using Get-DbaLogin on the pipeline to get all Disabled logins that have access on servers sql2016 or sql2014. + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Login, + [object[]]$IncludeFilter, + [object[]]$ExcludeLogin, + [object[]]$ExcludeFilter, + [switch]$NoSystem, + [switch]$SQLLogins, + [switch]$WindowsLogins, + [switch]$HasAccess, + [switch]$Locked, + [switch]$Disabled, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $serverLogins = $server.Logins + + if ($Login) { + $serverLogins = $serverLogins | Where-Object Name -in $Login + } + + if ($NoSystem) { + $serverLogins = $serverLogins | Where-Object IsSystemObject -eq $false + } + + if ($SQLLogins) { + $serverLogins = $serverLogins | Where-Object LoginType -eq 'SqlLogin' + } + + if ($WindowsLogins) { + $serverLogins = $serverLogins | Where-Object LoginType -eq 'WindowsUser' + } + + if ($IncludeFilter) { + $serverLogins = $serverLogins | Where-Object { + foreach ($filter in $IncludeFilter) { + if ($_.Name -like $filter) { + return $true; + } + } + } + } + + if ($ExcludeLogin) { + $serverLogins = $serverLogins | Where-Object Name -NotIn $ExcludeLogin + } + + if ($ExcludeFilter) { + foreach ($filter in $ExcludeFilter) { + $serverLogins = $serverLogins | Where-Object Name -NotLike $filter + } + } + + if ($HasAccess) { + $serverLogins = $serverLogins | Where-Object HasAccess + } + + if ($Locked) { + $serverLogins = $serverLogins | Where-Object IsLocked + } + + if ($Disabled) { + $serverLogins = $serverLogins | Where-Object IsDisabled + } + + foreach ($serverLogin in $serverlogins) { + Write-Message -Level Verbose -Message "Processing $serverLogin on $instance" + + if ($server.VersionMajor -gt 9) { + # There's no reliable method to get last login time with SQL Server 2000, so only show on 2005+ + Write-Message -Level Verbose -Message "Getting last login time" + $sql = "SELECT MAX(login_time) AS [login_time] FROM sys.dm_exec_sessions WHERE login_name = '$($serverLogin.name)'" + Add-Member -Force -InputObject $serverLogin -MemberType NoteProperty -Name LastLogin -Value $server.ConnectionContext.ExecuteScalar($sql) + } + else { + Add-Member -Force -InputObject $serverLogin -MemberType NoteProperty -Name LastLogin -Value $null + } + + Add-Member -Force -InputObject $serverLogin -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $serverLogin -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $serverLogin -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + + Select-DefaultView -InputObject $serverLogin -Property ComputerName, InstanceName, SqlInstance, Name, LoginType, CreateDate, LastLogin, HasAccess, IsLocked, IsDisabled + } + } + } +} +function Get-DbaLogShippingError { + <# + .SYNOPSIS + Get-DbaLogShippingError returns all the log shipping errors that occurred + + .DESCRIPTION + When your log shipping fails it's sometimes hard to see why is fails. + Using this function you'll be able to find out what went wrong in a short amount of time. + + .PARAMETER SqlInstance + SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Allows you to filter the results to only return the databases you're interested in. This can be one or more values separated by commas. + This is not a wildcard and should be the exact database name. See examples for more info. + + .PARAMETER ExcludeDatabase + Allows you to filter the results to only return the databases you're not interested in. This can be one or more values separated by commas. + This is not a wildcard and should be the exact database name. + + .PARAMETER Action + Filter to get the log shipping action that has occurred like Backup, Copy, Restore. + By default all the actions are returned. + + .PARAMETER DateTimeFrom + Filter the results based on the date starting from datetime X + + .PARAMETER DateTimeTo + Filter the results based on the date ending with datetime X + + .PARAMETER Primary + Allows to filter the results to only return values that apply to the primary instance. + + .PARAMETER Secondary + Allows to filter the results to only return values that apply to the secondary instance. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: LogShipping + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaLogShippingError + + .EXAMPLE + Get-DbaLogShippingError -SqlInstance sql1 + + Get all the log shipping errors that occurred + + .EXAMPLE + Get-DbaLogShippingError -SqlInstance sql1 -Action Backup + + Get the errors that have something to do with the backup of the databases + + .EXAMPLE + Get-DbaLogShippingError -SqlInstance sql1 -Secondary + + Get the errors that occurred on the secondary instance. + This will return the copy of the restore actions because those only occur on the secondary instance + + .EXAMPLE + Get-DbaLogShippingError -SqlInstance sql1 -DateTimeFrom "01/05/2018" + + Get the errors that have occurred from "01/05/2018". This can also be of format "yyyy-MM-dd" + + .EXAMPLE + Get-DbaLogShippingError -SqlInstance sql1 -Secondary -DateTimeFrom "01/05/2018" -DateTimeTo "2018-01-07" + + Get the errors that have occurred between "01/05/2018" and "01/07/2018". + See that is doesn't matter how the date is represented. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Database, + [string[]]$ExcludeDatabase, + [ValidateSet("Backup", "Copy", "Restore")] + [string[]]$Action, + [datetime]$DateTimeFrom, + [datetime]$DateTimeTo, + [switch]$Primary, + [switch]$Secondary, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + # Create array list to hold the results + $collection = New-Object System.Collections.ArrayList + + } + + process { + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.EngineEdition -match "Express") { + Write-Message -Level Warning -Message "$instance is Express Edition which does not support Log Shipping" + continue + } + + $query = " +CREATE TABLE #DatabaseID +( + DatabaseName VARCHAR(128), + DatabaseID UNIQUEIDENTIFIER, + Instance VARCHAR(20) +); + +INSERT INTO #DatabaseID +( + DatabaseName, + DatabaseID, + Instance +) +SELECT secondary_database, + secondary_id, + 'Secondary' +FROM msdb.dbo.log_shipping_secondary_databases; + + +INSERT INTO #DatabaseID +( + DatabaseName, + DatabaseID, + Instance +) +SELECT primary_database, + primary_id, + 'Primary' +FROM msdb.dbo.log_shipping_primary_databases; + + +SELECT di.DatabaseName, + di.Instance, + CASE lsmed.[agent_type] + WHEN 0 THEN + 'Backup' + WHEN 1 THEN + 'Copy' + WHEN 2 THEN + 'Restore' + ELSE + '' + END AS [Action], + lsmed.[session_id] AS SessionID, + lsmed.[sequence_number] AS SequenceNumber, + lsmed.[log_time] AS LogTime, + lsmed.[message] AS [Message] +FROM msdb.dbo.log_shipping_monitor_error_detail AS lsmed + INNER JOIN #DatabaseID AS di + ON di.DatabaseID = lsmed.agent_id +ORDER BY lsmed.[log_time], + lsmed.[database_name], + lsmed.[agent_type], + lsmed.[session_id], + lsmed.[sequence_number]; + +DROP TABLE #DatabaseID;" + + # Get the log shipping errors + $results = $server.Query($query) + + if ($results.Count -ge 1) { + + # Filter the results + if ($Database) { + $results = $results | Where-Object { $_.DatabaseName -in $Database } + } + + if ($Action) { + $results = $results | Where-Object { $_.Action -in $Action } + } + + if ($DateTimeFrom) { + $results = $results | Where-Object {$_.Logtime -ge $DateTimeFrom} + } + + if ($DateTimeTo) { + $results = $results | Where-Object {$_.Logtime -le $DateTimeTo} + } + + if ($Primary) { + $results = $results | Where-Object {$_.Instance -eq 'Primary'} + } + + if ($Secondary) { + $results = $results | Where-Object {$_.Instance -eq 'Secondary'} + } + + # Loop through each of the results + foreach ($result in $results) { + # Set up the custom object + $null = $collection.Add([PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $result.DatabaseName + Instance = $result.Instance + Action = $result.Action + SessionID = $result.SessionID + SequenceNumber = $result.SequenceNumber + LogTime = $result.LogTime + Message = $result.Message + }) + + } # for each result + } + else { + Write-Message -Message "No log shipping errors found" -Level Verbose + } + + } # foreach instance + + return $collection + + } # end process + +} + +function Get-DbaMaintenanceSolutionLog { + <# + .SYNOPSIS + Reads the log files generated by the IndexOptimize Agent Job from Ola Hallengren's MaintenanceSolution. + + .DESCRIPTION + Ola wrote a .sql script to get the content from the commandLog table. However, if LogToTable='N', there will be no logging in that table. This function reads the text files that are written in the SQL Instance's Log directory. + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER LogType + Accepts 'IndexOptimize', 'DatabaseBackup', 'DatabaseIntegrityCheck'. ATM only IndexOptimize parsing is available + + .PARAMETER Since + Consider only files generated since this date + + .PARAMETER Path + Where to search for log files. By default it's the SQL instance errorlogpath path + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Ola, Maintenance + Author: Klaas Vandenberghe ( @powerdbaklaas ) + Author: Simone Bizzotto ( @niphlod ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaMaintenanceSolutionLog + + .EXAMPLE + Get-DbaMaintenanceSolutionLog -SqlInstance sqlserver2014a + + Gets the outcome of the IndexOptimize job on sql instance sqlserver2014a. + + .EXAMPLE + Get-DbaMaintenanceSolutionLog -SqlInstance sqlserver2014a -SqlCredential $credential + + Gets the outcome of the IndexOptimize job on sqlserver2014a, using SQL Authentication. + + .EXAMPLE + 'sqlserver2014a', 'sqlserver2020test' | Get-DbaMaintenanceSolutionLog + + Gets the outcome of the IndexOptimize job on sqlserver2014a and sqlserver2020test. + + .EXAMPLE + Get-DbaMaintenanceSolutionLog -SqlInstance sqlserver2014a -Path 'D:\logs\maintenancesolution\' + + Gets the outcome of the IndexOptimize job on sqlserver2014a, reading the log files in their custom location. + + .EXAMPLE + Get-DbaMaintenanceSolutionLog -SqlInstance sqlserver2014a -Since '2017-07-18' + + Gets the outcome of the IndexOptimize job on sqlserver2014a, starting from july 18, 2017. + + .EXAMPLE + Get-DbaMaintenanceSolutionLog -SqlInstance sqlserver2014a -LogType IndexOptimize + + Gets the outcome of the IndexOptimize job on sqlserver2014a, the other options are not yet available! sorry + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateSet('IndexOptimize', 'DatabaseBackup', 'DatabaseIntegrityCheck')] + [string[]]$LogType = 'IndexOptimize', + [datetime]$Since, + [string]$Path, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function process-block ($block) { + $fresh = @{ + 'ObjectType' = $null + 'IndexType' = $null + 'ImageText' = $null + 'NewLOB' = $null + 'FileStream' = $null + 'ColumnStore' = $null + 'AllowPageLocks' = $null + 'PageCount' = $null + 'Fragmentation' = $null + 'Error' = $null + } + foreach ($l in $block) { + $splitted = $l -split ': ', 2 + if (($splitted.Length -ne 2) -or ($splitted[0].length -gt 20)) { + if ($null -eq $fresh['Error']) { + $fresh['Error'] = New-Object System.Collections.ArrayList + } + $null = $fresh['Error'].Add($l) + continue + } + $k = $splitted[0] + $v = $splitted[1] + if ($k -eq 'Date and Time') { + # this is the end date, we already parsed the start date of the block + if ($fresh.ContainsKey($k)) { + continue + } + } + $fresh[$k] = $v + } + if ($fresh.ContainsKey('Command')) { + if ($fresh['Command'] -match '(SET LOCK_TIMEOUT (?\d+); )?ALTER INDEX \[(?[^\]]+)\] ON \[(?[^\]]+)\]\.\[(?[^]]+)\]\.\[(?[^\]]+)\] (?[^\ ]+)( PARTITION = (?\d+))? WITH \((?[^\)]+)') { + $fresh['Index'] = $Matches.index + $fresh['Statistics'] = $null + $fresh['Schema'] = $Matches.Schema + $fresh['Table'] = $Matches.Table + $fresh['Action'] = $Matches.action + $fresh['Options'] = $Matches.options + $fresh['Timeout'] = $Matches.timeout + $fresh['Partition'] = $Matches.partition + } + elseif ($fresh['Command'] -match '(SET LOCK_TIMEOUT (?\d+); )?UPDATE STATISTICS \[(?[^\]]+)\]\.\[(?[^]]+)\]\.\[(?
[^\]]+)\] \[(?[^\]]+)\]') { + $fresh['Index'] = $null + $fresh['Statistics'] = $Matches.stat + $fresh['Schema'] = $Matches.Schema + $fresh['Table'] = $Matches.Table + $fresh['Action'] = $null + $fresh['Options'] = $null + $fresh['Timeout'] = $Matches.timeout + $fresh['Partition'] = $null + } + } + if ($fresh.ContainsKey('Comment')) { + $commentparts = $fresh['Comment'] -split ', ' + foreach ($part in $commentparts) { + $indkey, $indvalue = $part -split ': ', 2 + if ($fresh.ContainsKey($indkey)) { + $fresh[$indkey] = $indvalue + } + } + } + if ($null -ne $fresh['Error']) { + $fresh['Error'] = $fresh['Error'] -join "`n" + } + + return $fresh + } + } + process { + foreach ($instance in $sqlinstance) { + $logdir = $logfiles = $null + $computername = $instance.ComputerName + Write-Message -Level Verbose -Message "Connecting to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Can't connect to $instance" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($logtype -ne 'IndexOptimize') { + Write-Message -Level Warning -Message "Parsing $logtype is not supported at the moment" + Continue + } + if ($Path) { + $logdir = Join-AdminUnc -Servername $server.netname -Filepath $Path + } + else { + $logdir = Join-AdminUnc -Servername $server.netname -Filepath $server.errorlogpath # -replace '^(.):', "\\$computername\`$1$" + } + if (!$logdir) { + Write-Message -Level Warning -Message "No log directory returned from $instance" + Continue + } + + Write-Message -Level Verbose -Message "Log directory on $computername is $logdir" + if (! (Test-Path $logdir)) { + Write-Message -Level Warning -Message "Directory $logdir is not accessible" + continue + } + $logfiles = [System.IO.Directory]::EnumerateFiles("$logdir", "IndexOptimize_*.txt") + if ($Since) { + $filteredlogs = @() + foreach ($l in $logfiles) { + $base = $($l.Substring($l.Length - 15, 15)) + try { + $datefile = [DateTime]::ParseExact($base, 'yyyyMMdd_HHmmss', $null) + } + catch { + $datefile = Get-ItemProperty -Path $l | select -ExpandProperty CreationTime + } + if ($datefile -gt $since) { + $filteredlogs += $l + } + } + $logfiles = $filteredlogs + } + if (! $logfiles.count -ge 1) { + Write-Message -Level Warning -Message "No log files returned from $computername" + Continue + } + $instanceinfo = @{ } + $instanceinfo['ComputerName'] = $server.NetName + $instanceinfo['InstanceName'] = $server.ServiceName + $instanceinfo['SqlInstance'] = $server.Name + + foreach ($File in $logfiles) { + Write-Message -Level Verbose -Message "Reading $file" + $text = New-Object System.IO.StreamReader -ArgumentList "$File" + $block = New-Object System.Collections.ArrayList + $remember = @{} + while ($line = $text.ReadLine()) { + + $real = $line.Trim() + if ($real.Length -eq 0) { + $processed = process-block $block + if ('Procedure' -in $processed.Keys) { + $block = New-Object System.Collections.ArrayList + continue + } + if ('Database' -in $processed.Keys) { + Write-Message -Level Verbose -Message "Index and Stats Optimizations on Database $($processed.Database) on $computername" + $processed.Remove('Is accessible') + $processed.Remove('User access') + $processed.Remove('Date and time') + $processed.Remove('Standby') + $processed.Remove('Recovery Model') + $processed.Remove('Updateability') + $processed['Database'] = $processed['Database'].Trim('[]') + $remember = $processed.Clone() + } + else { + foreach ($k in $processed.Keys) { + $remember[$k] = $processed[$k] + } + $remember.Remove('Command') + $remember['StartTime'] = [dbadatetime]([DateTime]::ParseExact($remember['Date and time'] , "yyyy-MM-dd HH:mm:ss", $null)) + $remember.Remove('Date and time') + $remember['Duration'] = ($remember['Duration'] -as [timespan]) + [pscustomobject]$remember + } + $block = New-Object System.Collections.ArrayList + } + else { + $null = $block.Add($real) + } + } + $text.close() + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaMaxMemory { + <# + .SYNOPSIS + Gets the 'Max Server Memory' configuration setting and the memory of the server. Works on SQL Server 2000-2014. + + .DESCRIPTION + This command retrieves the SQL Server 'Max Server Memory' configuration setting as well as the total physical installed on the server. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: MaxMemory, Memory + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaMaxMemory + + .EXAMPLE + Get-DbaMaxMemory -SqlInstance sqlcluster,sqlserver2012 + + Get memory settings for all servers within the SQL Server Central Management Server "sqlcluster". + + .EXAMPLE + Get-DbaMaxMemory -SqlInstance sqlcluster | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } + + Find all servers in Server Central Management Server that have 'Max Server Memory' set to higher than the total memory of the server (think 2147483647) + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $totalMemory = $server.PhysicalMemory + + # Some servers under-report by 1MB. + if (($totalMemory % 1024) -ne 0) { + $totalMemory = $totalMemory + 1 + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TotalMB = [int]$totalMemory + SqlMaxMB = [int]$server.Configuration.MaxServerMemory.ConfigValue + } | Select-DefaultView -ExcludeProperty Server + } + } +} +#ValidationTags#Messaging,CodeStyle# +function Get-DbaMemoryUsage { + <# + .SYNOPSIS + Get amount of memory in use by *all* SQL Server components and instances + + .DESCRIPTION + Retrieves the amount of memory per performance counter. Default output includes columns Server, counter instance, counter, number of pages, memory in KB, memory in MB + SSAS and SSIS are included. + + SSRS does not have memory counters, only memory shrinks and memory pressure state. + + This function requires local admin role on the targeted computers. + + .PARAMETER ComputerName + The Windows Server that you are connecting to. Note that this will return all instances, but Out-GridView makes it easy to filter to specific instances. + + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Simple + Shows concise information including Server name, Database name, and the date the last time backups were performed + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Memory + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + SSIS Counters: https://msdn.microsoft.com/en-us/library/ms137622.aspx + + .LINK + https://dbatools.io/Get-DbaMemoryUsage + + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA + + Returns a custom object displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB + + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA\sql987 -Simple + + Returns a custom object with Server, counter instance, counter, number of pages, memory in KB, memory in MB + + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA\sql987 | Out-Gridview + + Returns a gridview displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("Host", "cn", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch]$Simple, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($Simple) { + $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' + $Plancounters = 'total\)\\cache pages' + $BufManpagecounters = 'Total pages' + $SSAScounters = '(\\memory usage)' + $SSIScounters = '(memory)' + } + else { + $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' + $Plancounters = '(cache pages|procedure plan|ad hoc sql plan|prepared SQL Plan)' + $BufManpagecounters = '(Free pages|Reserved pages|Stolen pages|Total pages|Database pages|target pages|extension .* pages)' + $SSAScounters = '(\\memory )' + $SSIScounters = '(memory)' + } + + $scriptblock = { + param ($Memcounters, + $Plancounters, + $BufManpagecounters, + $SSAScounters, + $SSIScounters) + Write-Verbose "Searching for Memory Manager Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet '*sql*:Memory Manager*' -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $Memcounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue + MemMB = $_.cookedvalue / 1024 + } + } + } + catch { + Write-Verbose "No Memory Manager Counters on $Computer" + } + + Write-Verbose "Searching for Plan Cache Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet '*sql*:Plan Cache*' -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $Plancounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $_.cookedvalue + MemKB = $_.cookedvalue * 8192 / 1024 + MemMB = $_.cookedvalue * 8192 / 1048576 + } + } + } + catch { + Write-Verbose "No Plan Cache Counters on $Computer" + } + + Write-Verbose "Searching for Buffer Manager Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "*Buffer Manager*" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $BufManpagecounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $_.cookedvalue + MemKB = $_.cookedvalue * 8192 / 1024.0 + MemMB = $_.cookedvalue * 8192 / 1048576.0 + } + } + } + catch { + Write-Verbose "No Buffer Manager Counters on $Computer" + } + + Write-Verbose "Searching for SSAS Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "MSAS*:Memory" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $SSAScounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue + MemMB = $_.cookedvalue / 1024 + } + } + } + catch { + Write-Verbose "No SSAS Counters on $Computer" + } + + Write-Verbose "Searching for SSIS Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "*SSIS*" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $SSIScounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue / 1024 + MemMB = $_.cookedvalue / 1024 / 1024 + } + } + } + catch { + Write-Verbose "No SSIS Counters on $Computer" + } + } + } + + process { + foreach ($Computer in $ComputerName.ComputerName) { + $reply = Resolve-DbaNetworkName -ComputerName $computer -Credential $Credential -ErrorAction SilentlyContinue + if ($reply.FullComputerName) { + $Computer = $reply.FullComputerName + try { + Write-Message -Level Verbose -Message "Connecting to $Computer" + Invoke-Command2 -ComputerName $Computer -Credential $Credential -ScriptBlock $scriptblock -argumentlist $Memcounters, $Plancounters, $BufManpagecounters, $SSAScounters, $SSIScounters + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + else { + Write-Message -Level Warning -Message "Can't resolve $Computer." + Continue + } + } + } +} +#ValidationTags#Messaging# +function Get-DbaMsdtc { + <# + .SYNOPSIS + Displays information about the Distributed Transaction Coordinator (MSDTC) on a server + + .DESCRIPTION + Returns a custom object with Computer name, state of the MSDTC Service, security settings of MSDTC and CID's + + Requires: Windows administrator access on Servers + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. + + .NOTES + Tags: Msdtc, dtc + Author: Klaas Vandenberghe ( powerdbaklaas ) + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaMsdtc + + .EXAMPLE + Get-DbaMsdtc -ComputerName srv0042 + + Get DTC status for the server srv0042 + + .EXAMPLE + $Computers = (Get-Content D:\configfiles\SQL\MySQLInstances.txt | % {$_.split('\')[0]}) + $Computers | Get-DbaMsdtc + + Get DTC status for all the computers in a .txt file + + .EXAMPLE + Get-DbaMsdtc -Computername $Computers | where { $_.dtcservicestate -ne 'running' } + + Get DTC status for all the computers where the MSDTC Service is not running + + .EXAMPLE + Get-DbaMsdtc -ComputerName srv0042 | Out-Gridview + + Get DTC status for the computer srv0042 and show in a grid view + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias('cn', 'host', 'Server')] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME + ) + + begin { + $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique + $query = "Select * FROM Win32_Service WHERE Name = 'MSDTC'" + $dtcSecurity = { + Get-ItemProperty -Path HKLM:\Software\Microsoft\MSDTC\Security | + Select-Object PSPath, PSComputerName, AccountName, networkDTCAccess, + networkDTCAccessAdmin, networkDTCAccessClients, networkDTCAccessInbound, + networkDTCAccessOutBound, networkDTCAccessTip, networkDTCAccessTransactions, XATransactions + } + $dtcCids = { + New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null + Get-ItemProperty -Path HKCR:\CID\*\Description | + Select-Object @{ l = 'Data'; e = { $_.'(default)' } }, @{ l = 'CID'; e = { $_.PSParentPath.split('\')[-1] } } + Remove-PSDrive -Name HKCR | Out-Null + } + } + process { + foreach ($computer in $ComputerName) { + $reg = $cids = $null + $cidHash = @{} + if ( Test-PSRemoting -ComputerName $computer ) { + $dtcservice = $null + Write-Message -Level Verbose -Message "Getting DTC on $computer via WSMan" + $dtcservice = Get-Ciminstance -ComputerName $computer -Query $query + if ( $null -eq $dtcservice ) { + Write-Warning "Can't connect to CIM on $computer via WSMan" + } + + Write-Message -Level Verbose -Message "Getting MSDTC Security Registry Values on $computer" + $reg = Invoke-Command -ComputerName $computer -ScriptBlock $dtcSecurity + if ( $null -eq $reg ) { + Write-Message -Level Warning -Message "Can't connect to MSDTC Security registry on $computer" + } + Write-Message -Level Verbose -Message "Getting MSDTC CID Registry Values on $computer" + $cids = Invoke-Command -ComputerName $computer -ScriptBlock $dtcCids + if ( $null -ne $cids ) { + foreach ($key in $cids) { + $cidHash.Add($key.Data, $key.CID) + } + } + else { + Write-Message -Level Warning -Message "Can't connect to MSDTC CID registry on $computer" + } + } + else { + Write-Message -Level Verbose -Message "PSRemoting is not enabled on $computer" + try { + Write-Message -Level Verbose -Message "Failed To get DTC via WinRM. Getting DTC on $computer via DCom" + $SessionParams = @{ } + $SessionParams.ComputerName = $Computer + $SessionParams.SessionOption = (New-CimSessionOption -Protocol Dcom) + $Session = New-CimSession @SessionParams + $dtcservice = Get-Ciminstance -CimSession $Session -Query $query + } + catch { + Stop-Function -Message "Can't connect to CIM on $computer via DCom" -Target $computer -ErrorRecord $_ -Continue + } + } + if ( $dtcservice ) { + [PSCustomObject]@{ + ComputerName = $dtcservice.PSComputerName + DTCServiceName = $dtcservice.DisplayName + DTCServiceState = $dtcservice.State + DTCServiceStatus = $dtcservice.Status + DTCServiceStartMode = $dtcservice.StartMode + DTCServiceAccount = $dtcservice.StartName + DTCCID_MSDTC = $cidHash['MSDTC'] + DTCCID_MSDTCUIS = $cidHash['MSDTCUIS'] + DTCCID_MSDTCTIPGW = $cidHash['MSDTCTIPGW'] + DTCCID_MSDTCXATM = $cidHash['MSDTCXATM'] + networkDTCAccess = $reg.networkDTCAccess + networkDTCAccessAdmin = $reg.networkDTCAccessAdmin + networkDTCAccessClients = $reg.networkDTCAccessClients + networkDTCAccessInbound = $reg.networkDTCAccessInbound + networkDTCAccessOutBound = $reg.networkDTCAccessOutBound + networkDTCAccessTip = $reg.networkDTCAccessTip + networkDTCAccessTransactions = $reg.networkDTCAccessTransactions + XATransactions = $reg.XATransactions + } + } + } + } +} +function Get-DbaNetworkActivity { + <# + .SYNOPSIS + Gets the Current traffic on every Network Interface on a computer. + + .DESCRIPTION + Gets the Current traffic on every Network Interface on a computer. + See https://msdn.microsoft.com/en-us/library/aa394293(v=vs.85).aspx + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Network + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaNetworkActivity + + .EXAMPLE + Get-DbaNetworkActivity -ComputerName sqlserver2014a + + Gets the Current traffic on every Network Interface on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaNetworkActivity + + Gets the Current traffic on every Network Interface on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaNetworkActivity -ComputerName sql1,sql2 | Out-Gridview + + Gets the Current traffic on every Network Interface on computers sql1 and sql2, and shows them in a grid view. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [string[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique + $sessionoption = New-CimSessionOption -Protocol DCom + } + PROCESS { + foreach ($computer in $ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ( $Server.FullComputerName ) { + $Computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan" + $CIMsession = New-CimSession -ComputerName $Computer -ErrorAction SilentlyContinue -Credential $Credential + if ( -not $CIMSession ) { + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan failed. Creating CIMSession on $computer over DCom" + $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential + } + if ( $CIMSession ) { + Write-Message -Level Verbose -Message "Getting properties for Network Interfaces on $computer" + $NICs = Get-CimInstance -CimSession $CIMSession -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface + $NICs | Add-Member -Force -MemberType ScriptProperty -Name ComputerName -Value { $computer } + $NICs | Add-Member -Force -MemberType ScriptProperty -Name Bandwith -Value { switch ( $this.CurrentBandWidth ) { 10000000000 { '10Gb' } 1000000000 { '1Gb' } 100000000 { '100Mb' } 10000000 { '10Mb' } 1000000 { '1Mb' } 100000 { '100Kb' } default { 'Low' } } } + foreach ( $NIC in $NICs ) { Select-DefaultView -InputObject $NIC -Property 'ComputerName', 'Name as NIC', 'BytesReceivedPersec', 'BytesSentPersec', 'BytesTotalPersec', 'Bandwidth'} + } #if CIMSession + else { + Write-Message -Level Warning -Message "Can't create CIMSession on $computer" + } + } #if computername + else { + Write-Message -Level Warning -Message "can't connect to $computer" + } + } #foreach computer + } #PROCESS +} #function +function Get-DbaNetworkCertificate { + <# +.SYNOPSIS +Simplifies finding computer certificates that are candidates for using with SQL Server's network encryption + +.DESCRIPTION +Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + +.PARAMETER ComputerName +The target SQL Server - defaults to localhost. If target is a cluster, you must specify the distinct nodes. + +.PARAMETER Credential +Allows you to login to $ComputerName using alternative credentials. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaNetworkCertificate +Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + +.EXAMPLE +Get-DbaNetworkCertificate -ComputerName sql2016 + +Gets computer certificates on sql2016 that are being used for SQL Server network encryption + +#> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($computer in $computername) { + + Write-Message -Level Verbose -Message "Connecting to SQL WMI on $($computer.ComputerName)" + try { + $sqlwmis = Invoke-ManagedComputerCommand -ComputerName $computer.ComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -match "SQL Server \(" + } + catch { + Stop-Function -Message $_ -Target $sqlwmi -Continue + } + + foreach ($sqlwmi in $sqlwmis) { + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Write-Message -Level Warning -Message "Can't find instance $vsname on $env:COMPUTERNAME" + return + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $computer } + + Write-Message -Level Verbose -Message "Regroot: $regroot" + Write-Message -Level Verbose -Message "ServiceAcct: $serviceaccount" + Write-Message -Level Verbose -Message "InstanceName: $instancename" + Write-Message -Level Verbose -Message "VSNAME: $vsname" + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$regroot\MSSQLServer\SuperSocketNetLib" + + $thumbprint = (Get-ItemProperty -Path $regpath -Name Certificate -ErrorAction SilentlyContinue).Certificate + + try { + $cert = Get-ChildItem Cert:\LocalMachine -Recurse -ErrorAction Stop | Where-Object Thumbprint -eq $Thumbprint + } + catch { + # Don't care - sometimes there's errors that are thrown for apparent good reason + } + + if (!$cert) { continue } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + FriendlyName = $cert.FriendlyName + DnsNameList = $cert.DnsNameList + Thumbprint = $cert.Thumbprint + Generated = $cert.NotBefore + Expires = $cert.NotAfter + IssuedTo = $cert.Subject + IssuedBy = $cert.Issuer + Certificate = $cert + } + } + + Write-Message -Level Verbose -Message "Connecting to $computer to get a list of certs" + try { + Invoke-Command2 -ComputerName $computer.ComputerName -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -ExcludeProperty Certificate + } + catch { + Stop-Function -Message $_ -ErrorRecord $_ -Target $ComputerName -Continue + } + } + } + } +} +function Get-DbaOpenTransaction { + <# + .SYNOPSIS + Displays all open transactions. + + .DESCRIPTION + This command is based on open transaction script published by Paul Randal. + Reference: https://www.sqlskills.com/blogs/paul/script-open-transactions-with-text-and-plans/ + + .PARAMETER SqlInstance + The SQL Server instance + + .PARAMETER SqlCredential + Connect using alternative credentials + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Process, Session, ActivityMonitor + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaOpenTransaction + + .EXAMPLE + Get-DbaOpenTransaction -SqlInstance sqlserver2014a + + Returns open transactions for sqlserver2014a + + .EXAMPLE + Get-DbaOpenTransaction -SqlInstance sqlserver2014a -SqlCredential (Get-Credential sqladmin) + + Logs into sqlserver2014a using the login "sqladmin" + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + begin { + $sql = " + SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + [s_tst].[session_id] as Spid, + [s_es].[login_name] as Login, + DB_NAME (s_tdt.database_id) AS [Database], + [s_tdt].[database_transaction_begin_time] AS [BeginTime], + [s_tdt].[database_transaction_log_bytes_used] AS [LogBytesUsed], + [s_tdt].[database_transaction_log_bytes_reserved] AS [LogBytesReserved], + [s_est].text AS [LastQuery], + [s_eqp].[query_plan] AS [LastPlan] + FROM + sys.dm_tran_database_transactions [s_tdt] + JOIN + sys.dm_tran_session_transactions [s_tst] + ON + [s_tst].[transaction_id] = [s_tdt].[transaction_id] + JOIN + sys.[dm_exec_sessions] [s_es] + ON + [s_es].[session_id] = [s_tst].[session_id] + JOIN + sys.dm_exec_connections [s_ec] + ON + [s_ec].[session_id] = [s_tst].[session_id] + LEFT OUTER JOIN + sys.dm_exec_requests [s_er] + ON + [s_er].[session_id] = [s_tst].[session_id] + CROSS APPLY + sys.dm_exec_sql_text ([s_ec].[most_recent_sql_handle]) AS [s_est] + OUTER APPLY + sys.dm_exec_query_plan ([s_er].[plan_handle]) AS [s_eqp] + ORDER BY + [BeginTime] ASC" + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $server.Query($sql) + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaOperatingSystem { + <# + .SYNOPSIS + Gets operating system information from the server. + + .DESCRIPTION + Gets operating system information from the server and returns as an object. + + .PARAMETER ComputerName + Target computer(s). If no computer name is specified, the local computer is targeted + + .PARAMETER Credential + Alternate credential object to use for accessing the target computer(s). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ServerInfo, OperatingSystem + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https: //dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaOperatingSystem + + .EXAMPLE + Get-DbaOperatingSystem + + Returns information about the local computer's operating system + + .EXAMPLE + Get-DbaOperatingSystem -ComputerName sql2016 + + Returns information about the sql2016's operating system + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($computer in $ComputerName) { + Write-Message -Level Verbose -Message "Attempting to connect to $computer" + $server = Resolve-DbaNetworkName -ComputerName $computer.ComputerName -Credential $Credential + + $computerResolved = $server.FullComputerName + + if (!$computerResolved) { + Write-Message -Level Warning -Message "Unable to resolve hostname of $computer. Skipping." + continue + } + + try { + $psVersion = Invoke-Command2 -ComputerName $computerResolved -Credential $Credential -ScriptBlock { $PSVersionTable.PSVersion } + } + catch { + Stop-Function -Message "Failure collecting PowerShell version on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $os = Get-DbaCmObject -ClassName Win32_OperatingSystem -ComputerName $computerResolved -Credential $Credential -EnableException + } + else { + $os = Get-DbaCmObject -ClassName Win32_OperatingSystem -ComputerName $computerResolved -EnableException + } + } + catch { + Stop-Function -Message "Failure collecting OS information on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $tz = Get-DbaCmObject -ClassName Win32_TimeZone -ComputerName $computerResolved -Credential $Credential -EnableException + } + else { + $tz = Get-DbaCmObject -ClassName Win32_TimeZone -ComputerName $computerResolved -EnableException + } + } + catch { + Stop-Function -Message "Failure collecting TimeZone information on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $powerPlan = Get-DbaCmObject -ClassName Win32_PowerPlan -Namespace "root\cimv2\power" -ComputerName $computerResolved -Credential $Credential -EnableException | Select-Object ElementName, InstanceId, IsActive + } + else { + $powerPlan = Get-DbaCmObject -ClassName Win32_PowerPlan -Namespace "root\cimv2\power" -ComputerName $computerResolved -EnableException | Select-Object ElementName, InstanceId, IsActive + } + } + catch { + Stop-Function -Message "Failure collecting PowerPlan information on $computer" -Target $computer -ErrorRecord $_ + return + } + + $activePowerPlan = ($powerPlan | Where-Object IsActive).ElementName -join ',' + $language = Get-Language $os.OSLanguage + + [PSCustomObject]@{ + ComputerName = $computerResolved + Manufacturer = $os.Manufacturer + Organization = $os.Organization + Architecture = $os.OSArchitecture + Version = $os.Version + Build = $os.BuildNumber + Caption = $os.Caption + InstallDate = [DbaDateTime]$os.InstallDate + LastBootTime = [DbaDateTime]$os.LastBootUpTime + LocalDateTime = [DbaDateTime]$os.LocalDateTime + PowerShellVersion = "$($psVersion.Major).$($psVersion.Minor)" + TimeZone = $tz.Caption + TimeZoneStandard = $tz.StandardName + TimeZoneDaylight = $tz.DaylightName + BootDevice = $os.BootDevice + TotalVisibleMemory = [DbaSize]($os.TotalVisibleMemorySize * 1024) + FreePhysicalMemory = [DbaSize]($os.FreePhysicalMemory * 1024) + TotalVirtualMemory = [DbaSize]($os.TotalVirtualMemorySize * 1024) + FreeVirtualMemory = [DbaSize]($os.FreeVirtualMemory * 1024) + ActivePowerPlan = $activePowerPlan + Language = $language.Name + LanguageId = $language.LCID + LanguageKeyboardLayoutId = $language.KeyboardLayoutId + LanguageTwoLetter = $language.TwoLetterISOLanguageName + LanguageThreeLetter = $language.ThreeLetterISOLanguageName + LanguageAlias = $language.DisplayName + LanguageNative = $language.NativeName + CodeSet = $os.CodeSet + CountryCode = $os.CountryCode + Locale = $os.Locale + } | Select-DefaultView -Property ComputerName, Manufacturer, Organization, Architecture, Version, Caption, LastBootTime, LocalDateTime, PowerShellVersion, TimeZone, TotalVisibleMemory, ActivePowerPlan, LanguageNative + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaOrphanUser { + <# + .SYNOPSIS + Get orphaned users. + + .DESCRIPTION + An orphan user is defined by a user that does not have their matching login. (Login property = ""). + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Orphan, Database, User, Security, Login + Author: Claudio Silva (@ClaudioESSilva) + Author: Garry Bargsley (@gbargsley) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + .LINK + + https://dbatools.io/Get-DbaOrphanUser + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 + Finds all orphan users without matching Logins in all databases present on server 'localhost\sql2016'. + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 -SqlCredential $cred + Finds all orphan users without matching Logins in all databases present on server 'localhost\sql2016'. SQL Server authentication will be used in connecting to the server. + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 -Database db1 + Finds orphan users without matching Logins in the db1 database present on server 'localhost\sql2016'. + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $SqlInstance." + continue + } + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($DatabaseCollection.Count -gt 0) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + Write-Message -Level Verbose -Message "Validating users on database '$db'." + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false } + + if ($UsersToWork.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found" + foreach ($user in $UsersToWork) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $user.Name + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database '$db'." + } + #reset collection + $UsersToWork = $null + } + catch { + Stop-Function -Message $_ -Continue + } + } + } + else { + Write-Message -Level VeryVerbose -Message "There are no databases to analyse." + } + } + } + +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Get-DbaPageFileSetting { +<# + .SYNOPSIS + Returns information on the pagefile configuration of the target computer. + + .DESCRIPTION + This command uses CIM (or other, related computer management tools) to detect the pagefile configuration of the target compuer(s). + Note that this may require local administrator privileges for the relevant computers. + + .PARAMETER ComputerName + The Server that you're connecting to. + This can be the name of a computer, a SMO object, an IP address, an AD COmputer object, a connection string or a SQL Instance. + + .PARAMETER Credential + Credential object used to connect to the Computer as a different user + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CIM + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaPageFileSetting -ComputerName ServerA,ServerB + + Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA and ServerB + + .EXAMPLE + 'ServerA' | Get-DbaPageFileSetting + + Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA + + .LINK + https://dbatools.io/Get-DbaPageFileSetting +#> + + [CmdletBinding()] + param ( + [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("cn", "host", "ServerInstance", "Server", "SqlServer")] + [DbaInstance]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($computer in $ComputerName) { + Write-Message -Level VeryVerbose -Message "Connecting to $($computer.ComputerName)" -Target $computer + $splatDbaCmObject = @{ + ComputerName = $computer + EnableException = $true + } + if ($Credential) { $splatDbaCmObject["Credential"] = $Credential } + + try { + $compSys = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_computersystem" + if (-not $CompSys.automaticmanagedpagefile) { + $pagefiles = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefile" + $pagefileUsages = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefileUsage" + $pagefileSettings = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefileSetting" + } + } + catch { + Stop-Function -Message "Failed to retrieve information from $($computer.ComputerName)" -ErrorRecord $_ -Target $computer -Continue + } + + if (-not $CompSys.automaticmanagedpagefile) { + foreach ($file in $pagefiles) { + $settings = $pagefileSettings | Where-Object Name -EQ $file.Name + $usage = $pagefileUsages | Where-Object Name -EQ $file.Name + + # pagefile is not automatic managed, so return settings + New-Object Sqlcollaborative.Dbatools.Computer.PageFileSetting -Property @{ + ComputerName = $computer.ComputerName + AutoPageFile = $CompSys.automaticmanagedpagefile + FileName = $file.name + Status = $file.status + SystemManaged = ($settings.InitialSize -eq 0) -and ($settings.MaximumSize -eq 0) + LastModified = $file.LastModified + LastAccessed = $file.LastAccessed + AllocatedBaseSize = $usage.AllocatedBaseSize # in MB, between Initial and Maximum Size + InitialSize = $settings.InitialSize # in MB + MaximumSize = $settings.MaximumSize # in MB + PeakUsage = $usage.peakusage # in MB + CurrentUsage = $usage.currentusage # in MB + } + } + } + else { + # pagefile is automatic managed, so there are no settings + New-Object Sqlcollaborative.Dbatools.Computer.PageFileSetting -Property @{ + ComputerName = $computer + AutoPageFile = $CompSys.automaticmanagedpagefile + FileName = $null + Status = $null + SystemManaged = $null + LastModified = $null + LastAccessed = $null + AllocatedBaseSize = $null + InitialSize = $null + MaximumSize = $null + PeakUsage = $null + CurrentUsage = $null + } + } + } + } +} +function Get-DbaPermission { + <# + .SYNOPSIS + Get a list of Server and Database level permissions + + .DESCRIPTION + Retrieves a list of permissions + + Permissions link principals to securables. + Principals exist on Windows, Instance and Database level. + Securables exist on Instance and Database level. + A permission state can be GRANT, DENY or REVOKE. + The permission type can be SELECT, CONNECT, EXECUTE and more. + + See https://msdn.microsoft.com/en-us/library/ms191291.aspx for more information + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER IncludeServerLevel + If this switch is enabled, information about Server Level Permissions will be output. + + .PARAMETER NoSystemObjects + If this switch is enabled, permissions on system securables will be excluded. + + .PARAMETER EnableException + If this switch is enabled exceptions will be thrown to the caller, which will need to perform its own exception processing. Otherwise, the function will try to catch the exception, interpret it and provide a friendly error message. + + .NOTES + Tags: Permissions, Databases + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPermission + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 + + Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable. + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 | Format-Table -AutoSize + + Returns a formatted table displaying Server, Database, permission state, permission type, grantee, granteetype, securable and securabletype. + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 -NoSystemObjects -IncludeServerLevel + + Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable + in all databases and on the server level, but not on system securables. + + .EXAMPLE + Get-DbaPermission -SqlInstance sql2016 -Database master + + Returns a custom object with permissions for the master database. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeServerLevel, + [switch]$NoSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ($NoSystemObjects) { + $ExcludeSystemObjectssql = "WHERE major_id > 0 " + } + + $ServPermsql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance + , [Database] = '' + , [PermState] = state_desc + , [PermissionName] = permission_name + , [SecurableType] = COALESCE(o.type_desc,sp.class_desc) + , [Securable] = CASE WHEN class = 100 THEN @@SERVERNAME + WHEN class = 105 THEN OBJECT_NAME(major_id) + ELSE OBJECT_NAME(major_id) + END + , [Grantee] = SUSER_NAME(grantee_principal_id) + , [GranteeType] = pr.type_desc + , [revokeStatement] = 'REVOKE ' + permission_name + ' ' + COALESCE(OBJECT_NAME(major_id),'') + ' FROM [' + SUSER_NAME(grantee_principal_id) + ']' + , [grantStatement] = 'GRANT ' + permission_name + ' ' + COALESCE(OBJECT_NAME(major_id),'') + ' TO [' + SUSER_NAME(grantee_principal_id) + ']' + FROM sys.server_permissions sp + JOIN sys.server_principals pr ON pr.principal_id = sp.grantee_principal_id + LEFT OUTER JOIN sys.all_objects o ON o.object_id = sp.major_id + + $ExcludeSystemObjectssql + + UNION ALL + SELECT SERVERPROPERTY('MachineName') AS ComputerName + , ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName + , SERVERPROPERTY('ServerName') AS SqlInstance + , [database] = '' + , [PermState] = 'GRANT' + , [PermissionName] = pb.[permission_name] + , [SecurableType] = pb.class_desc + , [Securable] = @@SERVERNAME + , [Grantee] = spr.name + , [GranteeType] = spr.type_desc + , [revokestatement] = '' + , [grantstatement] = '' + FROM sys.server_principals AS spr + INNER JOIN sys.fn_builtin_permissions('SERVER') AS pb ON + spr.[name]='bulkadmin' AND pb.[permission_name]='ADMINISTER BULK OPERATIONS' + OR + spr.[name]='dbcreator' AND pb.[permission_name]='CREATE ANY DATABASE' + OR + spr.[name]='diskadmin' AND pb.[permission_name]='ALTER RESOURCES' + OR + spr.[name]='processadmin' AND pb.[permission_name] IN ('ALTER ANY CONNECTION', 'ALTER SERVER STATE') + OR + spr.[name]='sysadmin' AND pb.[permission_name]='CONTROL SERVER' + OR + spr.[name]='securityadmin' AND pb.[permission_name]='ALTER ANY LOGIN' + OR + spr.[name]='serveradmin' AND pb.[permission_name] IN ('ALTER ANY ENDPOINT', 'ALTER RESOURCES','ALTER SERVER STATE', 'ALTER SETTINGS','SHUTDOWN', 'VIEW SERVER STATE') + OR + spr.[name]='setupadmin' AND pb.[permission_name]='ALTER ANY LINKED SERVER' + WHERE spr.[type]='R' + ;" + + $DBPermsql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance + , [Database] = DB_NAME() + , [PermState] = state_desc + , [PermissionName] = permission_name + , [SecurableType] = COALESCE(o.type_desc,dp.class_desc) + , [Securable] = CASE WHEN class = 0 THEN DB_NAME() + WHEN class = 1 THEN ISNULL(s.name + '.','')+OBJECT_NAME(major_id) + WHEN class = 3 THEN SCHEMA_NAME(major_id) + WHEN class = 6 THEN SCHEMA_NAME(t.schema_id)+'.' + t.name + END + , [Grantee] = USER_NAME(grantee_principal_id) + , [GranteeType] = pr.type_desc + , [revokeStatement] = 'REVOKE ' + permission_name + ' ON ' + isnull(schema_name(o.object_id)+'.','')+OBJECT_NAME(major_id)+ ' FROM [' + USER_NAME(grantee_principal_id) + ']' + , [grantStatement] = 'GRANT ' + permission_name + ' ON ' + isnull(schema_name(o.object_id)+'.','')+OBJECT_NAME(major_id)+ ' TO [' + USER_NAME(grantee_principal_id) + ']' + FROM sys.database_permissions dp + JOIN sys.database_principals pr ON pr.principal_id = dp.grantee_principal_id + LEFT OUTER JOIN sys.all_objects o ON o.object_id = dp.major_id + LEFT OUTER JOIN sys.schemas s ON s.schema_id = o.schema_id + LEFT OUTER JOIN sys.types t on t.user_type_id = dp.major_id + + $ExcludeSystemObjectssql + + UNION ALL + SELECT SERVERPROPERTY('MachineName') AS ComputerName + , ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName + , SERVERPROPERTY('ServerName') AS SqlInstance + , [database] = DB_NAME() + , [PermState] = '' + , [PermissionName] = p.[permission_name] + , [SecurableType] = p.class_desc + , [Securable] = DB_NAME() + , [Grantee] = dp.name + , [GranteeType] = dp.type_desc + , [revokestatement] = '' + , [grantstatement] = '' + FROM sys.database_principals AS dp + INNER JOIN sys.fn_builtin_permissions('DATABASE') AS p ON + dp.[name]='db_accessadmin' AND p.[permission_name] IN ('ALTER ANY USER', 'CREATE SCHEMA') + OR + dp.[name]='db_backupoperator' AND p.[permission_name] IN ('BACKUP DATABASE', 'BACKUP LOG', 'CHECKPOINT') + OR + dp.[name] IN ('db_datareader', 'db_denydatareader') AND p.[permission_name]='SELECT' + OR + dp.[name] IN ('db_datawriter', 'db_denydatawriter') AND p.[permission_name] IN ('INSERT', 'DELETE', 'UPDATE') + OR + dp.[name]='db_ddladmin' AND + p.[permission_name] IN ('ALTER ANY ASSEMBLY', 'ALTER ANY ASYMMETRIC KEY', + 'ALTER ANY CERTIFICATE', 'ALTER ANY CONTRACT', + 'ALTER ANY DATABASE DDL TRIGGER', 'ALTER ANY DATABASE EVENT', + 'NOTIFICATION', 'ALTER ANY DATASPACE', 'ALTER ANY FULLTEXT CATALOG', + 'ALTER ANY MESSAGE TYPE', 'ALTER ANY REMOTE SERVICE BINDING', + 'ALTER ANY ROUTE', 'ALTER ANY SCHEMA', 'ALTER ANY SERVICE', + 'ALTER ANY SYMMETRIC KEY', 'CHECKPOINT', 'CREATE AGGREGATE', + 'CREATE DEFAULT', 'CREATE FUNCTION', 'CREATE PROCEDURE', + 'CREATE QUEUE', 'CREATE RULE', 'CREATE SYNONYM', 'CREATE TABLE', + 'CREATE TYPE', 'CREATE VIEW', 'CREATE XML SCHEMA COLLECTION', + 'REFERENCES') + OR + dp.[name]='db_owner' AND p.[permission_name]='CONTROL' + OR + dp.[name]='db_securityadmin' AND p.[permission_name] IN ('ALTER ANY APPLICATION ROLE', 'ALTER ANY ROLE', 'CREATE SCHEMA', 'VIEW DEFINITION') + + WHERE dp.[type]='R' + AND dp.is_fixed_role=1 + ;" + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance." + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($IncludeServerLevel) { + Write-Message -Level Debug -Message "T-SQL: $ServPermsql" + $server.Query($ServPermsql) + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance." + + if ($db.IsAccessible -eq $false) { + Write-Warning "The database $db is not accessible. Skipping database." + Continue + } + + Write-Message -Level Debug -Message "T-SQL: $DBPermsql" + $db.ExecuteWithResults($DBPermsql).Tables.Rows + } + } + } +} +function Get-DbaPfAvailableCounter { + <# + .SYNOPSIS + Gathers list of all available counters on local or remote machines. + + .DESCRIPTION + Gathers list of all available counters on local or remote machines. Note, if you pass a credential object, it will be included in the output for easy reuse in your next piped command. + + Thanks to Daniel Streefkerk for this super fast way of counters + https://daniel.streefkerkonline.com/2016/02/18/use-powershell-to-list-all-windows-performance-counters-and-their-numeric-ids + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Pattern + Specify a pattern for filtering. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfAvailableCounter + + .EXAMPLE + Get-DbaPfAvailableCounter + + Gets all available counters on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* + + Gets all counters matching sql on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -ComputerName sql2017 -Pattern *sql* + + Gets all counters matching sql on the remote server sql2017. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* + + Gets all counters matching sql on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* | Add-DbaPfDataCollectorCounter -CollectorSet 'Test Collector Set' -Collector DataCollector01 + + Adds all counters matching "sql" to the DataCollector01 within the 'Test Collector Set' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:ComputerName, + [PSCredential]$Credential, + [string]$Pattern, + [switch]$EnableException + ) + begin { + $scriptblock = { + $counters = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009' -Name 'counter' | Select-Object -ExpandProperty Counter | + Where-Object { $_ -notmatch '[0-90000]' } | Sort-Object | Get-Unique + + foreach ($counter in $counters) { + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Name = $counter + Credential = $args + } + } + } + + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($computer in $ComputerName) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + + try { + if ($pattern) { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $credential -ErrorAction Stop | + Where-Object Name -match $pattern | Select-DefaultView -ExcludeProperty Credential + } + else { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $credential -ErrorAction Stop | + Select-DefaultView -ExcludeProperty Credential + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +function Get-DbaPfDataCollector { + <# + .SYNOPSIS + Gets Performance Monitor Data Collectors. + + .DESCRIPTION + Gets Performance Monitor Data Collectors. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollector + + .EXAMPLE + Get-DbaPfDataCollector + + Gets all Collectors on localhost. + + .EXAMPLE + Get-DbaPfDataCollector -ComputerName sql2017 + + Gets all Collectors on sql2017. + + .EXAMPLE + Get-DbaPfDataCollector -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets all Collectors for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Get-DbaPfDataCollector + + Gets all Collectors for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $columns = 'ComputerName', 'DataCollectorSet', 'Name', 'DataCollectorType', 'DataSourceName', 'FileName', 'FileNameFormat', 'FileNameFormatPattern', 'LatestOutputLocation', 'LogAppend', 'LogCircular', 'LogFileFormat', 'LogOverwrite', 'SampleInterval', 'SegmentMaxRecords', 'Counters' + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorSet -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorSetObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorSet." + return + } + } + + foreach ($set in $InputObject) { + $collectorxml = ([xml]$set.Xml).DataCollectorSet.PerformanceCounterDataCollector + foreach ($col in $collectorxml) { + if ($Collector -and $Collector -notcontains $col.Name) { + continue + } + + $outputlocation = $col.LatestOutputLocation + if ($outputlocation) { + $dir = ($outputlocation).Replace(':', '$') + $remote = "\\$($set.ComputerName)\$dir" + } + else { + $remote = $null + } + + [pscustomobject]@{ + ComputerName = $set.ComputerName + DataCollectorSet = $set.Name + Name = $col.Name + FileName = $col.FileName + DataCollectorType = $col.DataCollectorType + FileNameFormat = $col.FileNameFormat + FileNameFormatPattern = $col.FileNameFormatPattern + LogAppend = $col.LogAppend + LogCircular = $col.LogCircular + LogOverwrite = $col.LogOverwrite + LatestOutputLocation = $col.LatestOutputLocation + DataCollectorSetXml = $set.Xml + RemoteLatestOutputLocation = $remote + DataSourceName = $col.DataSourceName + SampleInterval = $col.SampleInterval + SegmentMaxRecords = $col.SegmentMaxRecords + LogFileFormat = $col.LogFileFormat + Counters = $col.Counter + CounterDisplayNames = $col.CounterDisplayName + CollectorXml = $col + DataCollectorObject = $true + Credential = $Credential + } | Select-DefaultView -Property $columns + } + } + } +} +function Get-DbaPfDataCollectorCounter { + <# + .SYNOPSIS + Gets Performance Counters. + + .DESCRIPTION + Gets Performance Counters. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER Counter + The Counter name to capture. This must be in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorCounter + + .EXAMPLE + Get-DbaPfDataCollectorCounter + + Gets all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017 + + Gets all counters for all Collector Sets on on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017 -Counter '\Processor(_Total)\% Processor Time' + + Gets the '\Processor(_Total)\% Processor Time' counter on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets all counters for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Get-DbaPfDataCollector | Get-DbaPfDataCollectorCounter + + Gets all counters for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [string[]]$Counter, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $columns = 'ComputerName', 'Name', 'DataCollectorSet', 'Counters', 'DataCollectorType', 'DataSourceName', 'FileName', 'FileNameFormat', 'FileNameFormatPattern', 'LatestOutputLocation', 'LogAppend', 'LogCircular', 'LogFileFormat', 'LogOverwrite', 'SampleInterval', 'SegmentMaxRecords' + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollector -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollector." + return + } + } + + foreach ($counterobject in $InputObject) { + foreach ($countername in $counterobject.Counters) { + if ($Counter -and $Counter -notcontains $countername) { continue } + [pscustomobject]@{ + ComputerName = $counterobject.ComputerName + DataCollectorSet = $counterobject.DataCollectorSet + DataCollector = $counterobject.Name + DataCollectorSetXml = $counterobject.DataCollectorSetXml + Name = $countername + FileName = $counterobject.FileName + CounterObject = $true + Credential = $Credential + } | Select-DefaultView -ExcludeProperty DataCollectorObject, Credential, CounterObject, DataCollectorSetXml + } + } + } +} +function Get-DbaPfDataCollectorCounterSample { + <# + .SYNOPSIS + Gets Performance Counter Samples. + + .DESCRIPTION + Gets Performance Counter Samples. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER Counter + The Counter name. This must be in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER Continuous + If this switch is enabled, samples will be retrieved continuously until you press CTRL+C. By default, this command gets only one counter sample. You can use the SampleInterval parameter to set the interval for continuous sampling. + + .PARAMETER ListSet + Gets the specified performance counter sets on the computers. Enter the names of the counter sets. Wildcards are permitted. + + .PARAMETER MaxSamples + Specifies the number of samples to get from each counter. The default is 1 sample. To get samples continuously (no maximum sample size), use the Continuous parameter. + + To collect a very large data set, consider running a Get-DbaPfDataCollectorCounterSample command as a Windows PowerShell background job. + + .PARAMETER SampleInterval + Specifies the time between samples in seconds. The minimum value and the default value are 1 second. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorCounter via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorCounterSample + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample + + Gets a single sample for all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -Counter '\Processor(_Total)\% Processor Time' + + Gets a single sample for all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017, sql2016 | Out-GridView -PassThru | Get-DbaPfDataCollectorCounterSample -MaxSamples 10 + + Gets 10 samples for all counters for all Collector Sets for servers sql2016 and sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -ComputerName sql2017 + + Gets a single sample for all counters for all Collector Sets on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets a single sample for all counters for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -CollectorSet 'System Correlation' + + Gets a single sample for all counters for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [string[]]$Counter, + [switch]$Continuous, + [switch[]]$ListSet, + [int]$MaxSamples, + [int]$SampleInterval, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if ($InputObject.Counter -and (Test-Bound -ParameterName Counter -Not)) { + $Counter = $InputObject.Counter + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorCounter -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector + } + } + + if ($InputObject) { + if (-not $InputObject.CounterObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorCounter." + return + } + } + + foreach ($counterobject in $InputObject) { + if ((Test-Bound -ParameterName Counter) -and ($Counter -notcontains $counterobject.Name)) { continue } + $params = @{ + Counter = $counterobject.Name + } + + if (-not ([dbainstance]$counterobject.ComputerName).IsLocalHost) { + $params.Add("ComputerName", $counterobject.ComputerName) + } + + if ($Credential) { + $params.Add("Credential", $Credential) + } + + if ($Continuous) { + $params.Add("Continuous", $Continuous) + } + + if ($ListSet) { + $params.Add("ListSet", $ListSet) + } + + if ($MaxSamples) { + $params.Add("MaxSamples", $MaxSamples) + } + + if ($SampleInterval) { + $params.Add("SampleInterval", $SampleInterval) + } + + if ($Continuous) { + Get-Counter @params + } + else { + try { + $pscounters = Get-Counter @params -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure for $($counterobject.Name) on $($counterobject.ComputerName)." -ErrorRecord $_ -Continue + } + + foreach ($pscounter in $pscounters) { + foreach ($sample in $pscounter.CounterSamples) { + [pscustomobject]@{ + ComputerName = $counterobject.ComputerName + DataCollectorSet = $counterobject.DataCollectorSet + DataCollector = $counterobject.DataCollector + Name = $counterobject.Name + Timestamp = $pscounter.Timestamp + Path = $sample.Path + InstanceName = $sample.InstanceName + CookedValue = $sample.CookedValue + RawValue = $sample.RawValue + SecondValue = $sample.SecondValue + MultipleCount = $sample.MultipleCount + CounterType = $sample.CounterType + SampleTimestamp = $sample.Timestamp + SampleTimestamp100NSec = $sample.Timestamp100NSec + Status = $sample.Status + DefaultScale = $sample.DefaultScale + TimeBase = $sample.TimeBase + Sample = $pscounter.CounterSamples + CounterSampleObject = $true + } | Select-DefaultView -ExcludeProperty Sample, CounterSampleObject + } + } + } + } + } +} +function Get-DbaPfDataCollectorSet { + <# + .SYNOPSIS + Gets Performance Monitor Data Collector Set. + + .DESCRIPTION + Gets Performance Monitor Data Collector Set. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector set name. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorSet + + .EXAMPLE + Get-DbaPfDataCollectorSet + + Gets all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2017 + + Gets all Collector Sets on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2017 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets the 'System Correlation' CollectorSet on sql2017 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet | Select * + + Displays extra columns and also exposes the original COM object in DataCollectorSetObject. + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [switch]$EnableException + ) + + begin { + $setscript = { + # Get names / status info + $schedule = New-Object -ComObject "Schedule.Service" + $schedule.Connect() + $folder = $schedule.GetFolder("Microsoft\Windows\PLA") + $tasks = @() + $tasknumber = 0 + $done = $false + do { + try { + $task = $folder.GetTasks($tasknumber) + $tasknumber++ + if ($task) { + $tasks += $task + } + } + catch { + $done = $true + } + } + while ($done -eq $false) + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) + + if ($args[0]) { + $tasks = $tasks | Where-Object Name -in $args[0] + } + + $sets = New-Object -ComObject Pla.DataCollectorSet + foreach ($task in $tasks) { + $setname = $task.Name + switch ($task.State) { + 0 { $state = "Unknown" } + 1 { $state = "Disabled" } + 2 { $state = "Queued" } + 3 { $state = "Ready" } + 4 { $state = "Running" } + } + + try { + # Query changes $sets so work from there + $sets.Query($setname, $null) + $set = $sets.PSObject.Copy() + + $outputlocation = $set.OutputLocation + $latestoutputlocation = $set.LatestOutputLocation + + if ($outputlocation) { + $dir = (Split-Path $outputlocation).Replace(':', '$') + $remote = "\\$env:COMPUTERNAME\$dir" + } + else { + $remote = $null + } + + if ($latestoutputlocation) { + $dir = ($latestoutputlocation).Replace(':', '$') + $remotelatest = "\\$env:COMPUTERNAME\$dir" + } + else { + $remote = $null + } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Name = $setname + LatestOutputLocation = $set.LatestOutputLocation + OutputLocation = $set.OutputLocation + RemoteOutputLocation = $remote + RemoteLatestOutputLocation = $remotelatest + RootPath = $set.RootPath + Duration = $set.Duration + Description = $set.Description + DescriptionUnresolved = $set.DescriptionUnresolved + DisplayName = $set.DisplayName + DisplayNameUnresolved = $set.DisplayNameUnresolved + Keywords = $set.Keywords + Segment = $set.Segment + SegmentMaxDuration = $set.SegmentMaxDuration + SegmentMaxSize = $set.SegmentMaxSize + SerialNumber = $set.SerialNumber + Server = $set.Server + Status = $set.Status + Subdirectory = $set.Subdirectory + SubdirectoryFormat = $set.SubdirectoryFormat + SubdirectoryFormatPattern = $set.SubdirectoryFormatPattern + Task = $set.Task + TaskRunAsSelf = $set.TaskRunAsSelf + TaskArguments = $set.TaskArguments + TaskUserTextArguments = $set.TaskUserTextArguments + Schedules = $set.Schedules + SchedulesEnabled = $set.SchedulesEnabled + UserAccount = $set.UserAccount + Xml = $set.Xml + Security = $set.Security + StopOnCompletion = $set.StopOnCompletion + State = $state.Trim() + DataCollectorSetObject = $true + TaskObject = $task + Credential = $args[1] + } + } + catch { + Write-Warning -Message "Issue with getting Collector Set $setname on $env:Computername : $_." + continue + } + } + } + + $columns = 'ComputerName', 'Name', 'DisplayName', 'Description', 'State', 'Duration', 'OutputLocation', 'LatestOutputLocation', + 'RootPath', 'SchedulesEnabled', 'Segment', 'SegmentMaxDuration', 'SegmentMaxSize', + 'SerialNumber', 'Server', 'StopOnCompletion', 'Subdirectory', 'SubdirectoryFormat', + 'SubdirectoryFormatPattern', 'Task', 'TaskArguments', 'TaskRunAsSelf', 'TaskUserTextArguments', 'UserAccount' + } + process { + foreach ($computer in $ComputerName.ComputerName) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $CollectorSet, $Credential -ErrorAction Stop | Select-DefaultView -Property $columns + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +function Get-DbaPfDataCollectorSetTemplate { + <# + .SYNOPSIS + Parses Perf Monitor templates. Defaults to parsing templates in the dbatools template repository (\bin\perfmontemplates\). + + .DESCRIPTION + Parses Perf Monitor XML templates. Defaults to parsing templates in the dbatools template repository (\bin\perfmontemplates\). + + .PARAMETER Path + The path to the template directory. Defaults to the dbatools template repository (\bin\perfmontemplates\). + + .PARAMETER Pattern + Specify a pattern for filtering. Alternatively, you can use Out-GridView -Passthru to select objects and pipe them to Import-DbaPfDataCollectorSetTemplate. + + .PARAMETER Template + Specifies one or more of the templates provided by dbatools. Press tab to cycle through the list to the options. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorSetTemplate + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate + + Returns information about all the templates in the local dbatools repository. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Out-GridView -PassThru | Import-DbaPfDataCollectorSetTemplate -ComputerName sql2017 | Start-DbaPfDataCollectorSet + + Allows you to select a template, then deploys it to sql2017 and immediately starts the DataCollectorSet. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Select-Object * + + Returns more information about the template, including the full path/filename. + #> + [CmdletBinding()] + param ( + [string[]]$Path = "$script:PSModuleRoot\bin\perfmontemplates\collectorsets", + [string]$Pattern, + [string[]]$Template, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\perfmontemplates\collectorsets.xml" + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($directory in $Path) { + $files = Get-ChildItem "$directory\*.xml" + + if ($Template) { + $files = $files | Where-Object BaseName -in $Template + } + + foreach ($file in $files) { + try { + $xml = [xml](Get-Content $file) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + foreach ($dataset in $xml.DataCollectorSet) { + $meta = $metadata | Where-Object Name -eq $dataset.name + if ($Pattern) { + if ( + ($dataset.Name -match $Pattern) -or + ($dataset.Description -match $Pattern) + ) { + [pscustomobject]@{ + Name = $dataset.name + Source = $meta.Source + UserAccount = $dataset.useraccount + Description = $dataset.Description + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, Path + } + } + else { + [pscustomobject]@{ + Name = $dataset.name + Source = $meta.Source + UserAccount = $dataset.useraccount + Description = $dataset.Description + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, Path + } + } + } + } + } +} +function Get-DbaPolicy { + <# + .SYNOPSIS + Returns polices from policy based management from an instance. + + .DESCRIPTION + Returns details of policies with the option to filter on Category and SystemObjects. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Policy + Filters results to only show specific policy + + .PARAMETER Category + Filters results to only show policies in the category selected + + .PARAMETER IncludeSystemObject + By default system objects are filtered out. Use this parameter to INCLUDE them . + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Stephen Bennett (https://sqlnotesfromtheunderground.wordpress.com/) + Tags: Policy, PoilcyBasedManagement + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPolicy + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 + + Returns all policies from sql2016 server + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 -SqlCredential $cred + + Uses a credential $cred to connect and return all policies from sql2016 instance + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 -Category MorningCheck + + Returns all policies from sql2016 server that part of the PolicyCategory MorningCheck +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [string[]]$Policy, + [string[]]$Category, + [switch]$IncludeSystemObject, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $sqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $server.ConnectionContext.SqlConnectionObject + # DMF is the Declarative Management Framework, Policy Based Management's old name + $store = New-Object Microsoft.SqlServer.Management.DMF.PolicyStore $sqlStoreConnection + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server -Continue + } + + $allpolicies = $store.Policies + + if (-not $IncludeSystemObject) { + $allpolicies = $allpolicies | Where-Object { $_.IsSystemObject -eq 0 } + } + + if ($Category) { + $allpolicies = $allpolicies | Where-Object { $_.PolicyCategory -in $Category } + } + + if ($Policy) { + $allpolicies = $allpolicies | Where-Object { $_.Name -in $Policy } + } + + foreach ($currentpolicy in $allpolicies) { + Write-Message -Level Verbose -Message "Processing $currentpolicy" + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $currentpolicy -ExcludeProperty HelpText, HelpLink, Urn, Properties, Metadata, Parent, IdentityKey, HasScript, PolicyEvaluationStarted, ConnectionProcessingStarted, TargetProcessed, ConnectionProcessingFinished, PolicyEvaluationFinished, PropertyMetadataChanged, PropertyChanged + } + } + } +} +function Get-DbaPrivilege { + <# + .SYNOPSIS + Gets the users with local privileges on one or more computers. + + .DESCRIPTION + Gets the users with local privileges 'Lock Pages in Memory', 'Instant File Initialization', 'Logon as Batch' on one or more computers. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Privilege + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPrivilege + + .EXAMPLE + Get-DbaPrivilege -ComputerName sqlserver2014a + + Gets the local privileges on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaPrivilege + + Gets the local privileges on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaPrivilege -ComputerName sql1,sql2 | Out-Gridview + + Gets the local privileges on computers sql1 and sql2, and shows them in a grid view. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [dbainstanceparameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + $ResolveSID = @" + function Convert-SIDToUserName ([string] `$SID ) { + `$objSID = New-Object System.Security.Principal.SecurityIdentifier (`"`$SID`") + `$objUser = `$objSID.Translate( [System.Security.Principal.NTAccount]) + `$objUser.Value + } +"@ + $ComputerName = $ComputerName.ComputerName | Select-Object -Unique + } + process { + foreach ($computer in $ComputerName) { + try { + Write-Message -Level Verbose -Message "Connecting to $computer" + if (Test-PSRemoting -ComputerName $Computer) { + Write-Message -Level Verbose -Message "Getting Privileges on $Computer" + $Priv = $null + $Priv = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); secedit /export /cfg $temp\secpolByDbatools.cfg > $NULL; + Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -match "SeBatchLogonRight" -or $_ -match 'SeManageVolumePrivilege' -or $_ -match 'SeLockMemoryPrivilege' } + } + + Write-Message -Level Verbose -Message "Getting Batch Logon Privileges on $Computer" + $BL = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -match "SeBatchLogonRight" }).substring(20).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + if ($BL.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Batch Logon Rights on $computer" + } + + Write-Message -Level Verbose -Message "Getting Instant File Initialization Privileges on $Computer" + $ifi = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -like 'SeManageVolumePrivilege*' }).substring(26).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + if ($ifi.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Instant File Initialization Rights on $computer" + } + + Write-Message -Level Verbose -Message "Getting Lock Pages in Memory Privileges on $Computer" + $lpim = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -like 'SeLockMemoryPrivilege*' }).substring(24).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + + if ($lpim.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Lock Pages in Memory Rights on $computer" + } + $users = @() + $BL + $ifi + $lpim | Select-Object -Unique + $users | ForEach-Object { + [PSCustomObject]@{ + ComputerName = $computer + User = $_ + LogonAsBatchPrivilege = $BL -contains $_ + InstantFileInitializationPrivilege = $ifi -contains $_ + LockPagesInMemoryPrivilege = $lpim -contains $_ + } + } + Write-Message -Level Verbose -Message "Removing secpol file on $computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); Remove-Item $temp\secpolByDbatools.cfg -Force > $NULL } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer + } + } + } +} +function Get-DbaProcess { + <# + .SYNOPSIS + This command displays SQL Server processes. + + .DESCRIPTION + This command displays processes associated with a spid, login, host, program or database. + + Thanks to Michael J Swart at https://sqlperformance.com/2017/07/sql-performance/find-database-connection-leaks for the query to get the last executed SQL statement, minutesasleep and host process ID. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Spid + Specifies one or more process IDs (Spid) to be displayed. Options for this parameter are auto-populated from the server. + + .PARAMETER Login + Specifies one or more Login names with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Hostname + Specifies one or more hostnames with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Program + Specifies one or more program names with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Database + Specifies one or more databases with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER ExcludeSpid + Specifies one ore more process IDs to exclude from display. Options for this parameter are auto-populated from the server. + + This is the last filter to run, so even if a Spid matches another filter, it will be excluded by this filter. + + .PARAMETER NoSystemSpid + If this switch is enabled, system Spids will be ignored. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Process, Session, ActivityMonitor + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaProcess + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Login base\ctrlb, sa + + Shows information about the processes for base\ctrlb and sa on sqlserver2014a. Windows Authentication is used in connecting to sqlserver2014a. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -SqlCredential $credential -Spid 56, 77 + + Shows information about the processes for spid 56 and 57. Uses alternative (SQL or Windows) credentials to authenticate to sqlserver2014a. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Program 'Microsoft SQL Server Management Studio' + + Shows information about the processes that were created in Microsoft SQL Server Management Studio. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Host workstationx, server100 + + Shows information about the processes that were initiated by hosts (computers/clients) workstationx and server 1000. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [int[]]$Spid, + [int[]]$ExcludeSpid, + [string[]]$Database, + [string[]]$Login, + [string[]]$Hostname, + [string[]]$Program, + [switch]$NoSystemSpid, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $sqlinstance) { + + Write-Message -Message "Attempting to connect to $instance." -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $instance : $_" -Target $instance -ErrorRecord $_ -Continue + } + + $sql = "SELECT datediff(minute, s.last_request_end_time, getdate()) as MinutesAsleep, s.session_id as spid, s.host_process_id as HostProcessId, t.text as Query, + s.login_time as LoginTime,s.client_version as ClientVersion, s.last_request_start_time as LastRequestStartTime, s.last_request_end_time as LastRequestEndTime, + c.net_transport as NetTransport, c.encrypt_option as EncryptOption, c.auth_scheme as AuthScheme, c.net_packet_size as NetPacketSize, c.client_net_address as ClientNetAddress + FROM sys.dm_exec_connections c join sys.dm_exec_sessions s on c.session_id = s.session_id cross apply sys.dm_exec_sql_text(c.most_recent_sql_handle) t" + + if ($server.VersionMajor -gt 8) { + $results = $server.Query($sql) + } + else { + $results = $null + } + + $allsessions = @() + + $processes = $server.EnumProcesses() + + if ($Login) { + $allsessions += $processes | Where-Object { $_.Login -in $Login -and $_.Spid -notin $allsessions.Spid } + } + + if ($Spid) { + $allsessions += $processes | Where-Object { ($_.Spid -in $Spid -or $_.BlockingSpid -in $Spid) -and $_.Spid -notin $allsessions.Spid } + } + + if ($Hostname) { + $allsessions += $processes | Where-Object { $_.Host -in $Hostname -and $_.Spid -notin $allsessions.Spid } + } + + if ($Program) { + $allsessions += $processes | Where-Object { $_.Program -in $Program -and $_.Spid -notin $allsessions.Spid } + } + + if ($Database) { + $allsessions += $processes | Where-Object { $Database -contains $_.Database -and $_.Spid -notin $allsessions.Spid } + } + + if (Test-Bound -not 'Login', 'Spid', 'ExcludeSpid', 'Hostname', 'Program', 'Database') { + $allsessions = $processes + } + + if ($NoSystemSpid -eq $true) { + $allsessions = $allsessions | Where-Object { $_.Spid -gt 50 } + } + + if ($Exclude) { + $allsessions = $allsessions | Where-Object { $Exclude -notcontains $_.SPID -and $_.Spid -notin $allsessions.Spid } + } + + foreach ($session in $allsessions) { + + if ($session.Status -eq "") { + $status = "sleeping" + } + else { + $status = $session.Status + } + + if ($session.Command -eq "") { + $command = "AWAITING COMMAND" + } + else { + $command = $session.Command + } + + $row = $results | Where-Object { $_.Spid -eq $session.Spid } + + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Parent -value $server + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Status -value $status + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Command -value $command + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name HostProcessId -value $row.HostProcessId + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name MinutesAsleep -value $row.MinutesAsleep + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LoginTime -value $row.LoginTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ClientVersion -value $row.ClientVersion + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastRequestStartTime -value $row.LastRequestStartTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastRequestEndTime -value $row.LastRequestEndTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name NetTransport -value $row.NetTransport + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name EncryptOption -value $row.EncryptOption + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name AuthScheme -value $row.AuthScheme + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name NetPacketSize -value $row.NetPacketSize + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ClientNetAddress -value $row.ClientNetAddress + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastQuery -value $row.Query + + Select-DefaultView -InputObject $session -Property ComputerName, InstanceName, SqlInstance, Spid, Login, LoginTime, Host, Database, BlockingSpid, Program, Status, Command, Cpu, MemUsage, LastRequestStartTime, LastRequestEndTime, MinutesAsleep, ClientNetAddress, NetTransport, EncryptOption, AuthScheme, NetPacketSize, ClientVersion, HostProcessId, IsSystem, LastQuery + } + } + } +} +function Get-DbaQueryExecutionTime { + <# +.SYNOPSIS +Displays Stored Procedures and Ad hoc queries with the highest execution times. Works on SQL Server 2008 and above. + +.DESCRIPTION +Quickly find slow query executions within a database. Results will include stored procedures and individual SQL statements. + +.PARAMETER SqlInstance +Allows you to specify a comma separated list of servers to query. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER MaxResultsPerDb +Allows you to limit the number of results returned, as many systems can have very large amounts of query plans. Default value is 100 results. + +.PARAMETER MinExecs +Allows you to limit the scope to queries that have been executed a minimum number of time. Default value is 100 executions. + +.PARAMETER MinExecMs +Allows you to limit the scope to queries with a specified average execution time. Default value is 500 (ms). + +.PARAMETER NoSystemDb +Allows you to suppress output on system databases + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Query, Performance +Author: Brandon Abshire, netnerds.net + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaQueryExecutionTime + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008, sqlserver2012 + +Return the top 100 slowest stored procedures or statements for servers sql2008 and sqlserver2012. + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008 -Database TestDB + +Return the top 100 slowest stored procedures or statements on server sql2008 for only the TestDB database. + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008 -Database TestDB -MaxResultsPerDb 100 -MinExecs 200 -MinExecMs 1000 + +Return the top 100 slowest stored procedures or statements on server sql2008 for only the TestDB database, +limiting results to queries with more than 200 total executions and an execution time over 1000ms or higher. +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Position = 1, Mandatory = $false)] + [int]$MaxResultsPerDb = 100, + [parameter(Position = 2, Mandatory = $false)] + [int]$MinExecs = 100, + [parameter(Position = 3, Mandatory = $false)] + [int]$MinExecMs = 500, + [parameter(Position = 4, Mandatory = $false)] + [switch]$NoSystemDb, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = ";With StatsCTE AS + ( + SELECT + DB_NAME() as DatabaseName, + (total_worker_time / execution_count) / 1000 AS AvgExec_ms , + execution_count , + max_worker_time / 1000 AS MaxExec_ms , + OBJECT_NAME(object_id) as ProcName, + object_id, + type_desc, + cached_time, + last_execution_time, + total_worker_time / 1000 as total_worker_time_ms, + total_elapsed_time / 1000 as total_elapsed_time_ms, + OBJECT_NAME(object_id) as SQLText, + OBJECT_NAME(object_id) as full_statement_text + FROM sys.dm_exec_procedure_stats + WHERE database_id = DB_ID()" + + If ($MinExecs) { $sql += "`n AND execution_count >= " + $MinExecs } + If ($MinExecMs) { $sql += "`n AND (total_worker_time / execution_count) / 1000 >= " + $MinExecMs } + + $sql += "`n UNION + SELECT + DB_NAME() as DatabaseName, + ( qs.total_worker_time / qs.execution_count ) / 1000 AS AvgExec_ms , + qs.execution_count , + qs.max_worker_time / 1000 AS MaxExec_ms , + OBJECT_NAME(st.objectid) as ProcName, + st.objectid as [object_id], + 'STATEMENT' as type_desc, + '1901-01-01 00:00:00' as cached_time, + qs.last_execution_time, + qs.total_worker_time / 1000 as total_worker_time_ms, + qs.total_elapsed_time / 1000 as total_elapsed_time_ms, + SUBSTRING(st.text, (qs.statement_start_offset/2)+1, 50) + '...' AS SQLText, + SUBSTRING(st.text, (qs.statement_start_offset/2)+1, + ((CASE qs.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END - qs.statement_start_offset)/2) + 1) AS full_statement_text + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) as pa + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as st + WHERE st.dbid = DB_ID() OR (pa.attribute = 'dbid' and pa.value = DB_ID())" + + If ($MinExecs) { $sql += "`n AND execution_count >= " + $MinExecs } + If ($MinExecMs) { $sql += "`n AND (total_worker_time / execution_count) / 1000 >= " + $MinExecMs } + + If ($MaxResultsPerDb) { $sql += ")`n SELECT TOP " + $MaxResultsPerDb } + Else { + $sql += ") + SELECT " + } + + $sql += "`n DatabaseName, + AvgExec_ms, + execution_count, + MaxExec_ms, + ProcName, + object_id, + type_desc, + cached_time, + last_execution_time, + total_worker_time_ms, + total_elapsed_time_ms, + SQLText, + full_statement_text + FROM StatsCTE " + + If ($MinExecs -or $MinExecMs) { + $sql += "`n WHERE `n" + + If ($MinExecs) { + $sql += " execution_count >= " + $MinExecs + } + + If ($MinExecMs -gt 0 -and $MinExecs) { + $sql += "`n AND AvgExec_ms >= " + $MinExecMs + } + elseif ($MinExecMs) { + $sql += "`n AvgExecs_ms >= " + $MinExecMs + } + } + + $sql += "`n ORDER BY AvgExec_ms DESC" + } + process { + if (!$MaxResultsPerDb -and !$MinExecs -and !$MinExecMs) { + Write-Warning "Results may take time, depending on system resources and size of buffer cache." + Write-Warning "Consider limiting results using -MaxResultsPerDb, -MinExecs and -MinExecMs parameters." + } + + foreach ($instance in $SqlInstance) { + Write-Verbose "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.versionMajor -lt 10) { + Write-Warning "This function does not support versions lower than SQL Server 2008 (v10). Skipping server $instance." + + Continue + } + + $dbs = $server.Databases + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($NoSystemDb) { + $dbs = $dbs | Where-Object { $_.IsSystemObject -eq $false } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Verbose "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Write-Warning "The database $db is not accessible. Skipping database." + Continue + } + + try { + foreach ($row in $db.ExecuteWithResults($sql).Tables.Rows) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DatabaseName + ProcName = $row.ProcName + ObjectID = $row.object_id + TypeDesc = $row.type_desc + Executions = $row.Execution_Count + AvgExecMs = $row.AvgExec_ms + MaxExecMs = $row.MaxExec_ms + CachedTime = $row.cached_time + LastExecTime = $row.last_execution_time + TotalWorkerTimeMs = $row.total_worker_time_ms + TotalElapsedTimeMs = $row.total_elapsed_time_ms + SQLText = $row.SQLText + FullStatementText = $row.full_statement_text + } | Select-DefaultView -ExcludeProperty FullStatementText + } + } + catch { + Write-Warning "Could not process $db on $instance. Exception: $_" + Continue + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaRegisteredServer { + <# + .SYNOPSIS + Gets list of SQL Server objects stored in SQL Server Central Management Server (CMS). + + .DESCRIPTION + Returns an array of servers found in the CMS. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Group + Specifies one or more groups to include from SQL Server Central Management Server. + + .PARAMETER ExcludeGroup + Specifies one or more Central Management Server groups to exclude. + + .PARAMETER ExcludeCmsServer + Deprecated, now follows the Microsoft convention of not including it by default. If you'd like to include the CMS Server, use -IncludeSelf + + .PARAMETER IncludeSelf + If this switch is enabled, the CMS server itself will be included in the results, along with all other Registered Servers. + + .PARAMETER ResolveNetworkName + If this switch is enabled, the NetBIOS name and IP address(es) of each server will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Bryan Hamby (@galador) + Tags: RegisteredServer, CMS + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServer + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a + + Gets a list of servers from the CMS on sqlserver2014a, using Windows Credentials. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -IncludeSelf + + Gets a list of servers from the CMS on sqlserver2014a and includes sqlserver2014a in the output results. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -SqlCredential $credential | Select-Object -Unique -ExpandProperty ServerName + + Returns only the server names from the CMS on sqlserver2014a, using SQL Authentication to authenticate to the server. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR, Accounting + + Gets a list of servers in the HR and Accounting groups from the CMS on sqlserver2014a. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR\Development + + Returns a list of servers in the HR and sub-group Development from the CMS on sqlserver2014a. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Groups")] + [object[]]$Group, + [object[]]$ExcludeGroup, + [switch]$IncludeSelf, + [switch]$ExcludeCmsServer, + [switch]$ResolveNetworkName, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function Find-CmsGroup { + [OutputType([object[]])] + [cmdletbinding()] + param( + $CmsGrp, + $Base = $null, + $Stopat + ) + $results = @() + foreach ($el in $CmsGrp) { + if ($null -eq $Base -or [string]::IsNullOrWhiteSpace($Base) ) { + $partial = $el.name + } + else { + $partial = "$Base\$($el.name)" + } + if ($partial -eq $Stopat) { + return $el + } + else { + foreach ($elg in $el.ServerGroups) { + $results += Find-CmsGroup -CmsGrp $elg -Base $partial -Stopat $Stopat + } + } + } + return $results + } + + $defaults = @() + if ($ResolveNetworkName) { + $defaults += 'ComputerName', 'FQDN', 'IPAddress' + } + $defaults += 'Name', 'ServerName', 'Description', 'ServerType', 'SecureConnectionString' + + } + + process { + if (Test-FunctionInterrupt) { + return + } + + $servers = @() + foreach ($instance in $SqlInstance) { + try { + $cmsStore = Get-DbaRegisteredServersStore -SqlInstance $instance -SqlCredential $SqlCredential -EnableException + } + catch { + Stop-Function -Message "Cannot access Central Management Server '$instance'." -ErrorRecord $_ -Continue + } + + if (Test-Bound -ParameterName ExcludeGroup) { + $Group = ($cmsStore.DatabaseEngineServerGroup.ServerGroups | Where-Object Name -notin $ExcludeGroup).Name + } + + if ($Group) { + foreach ($currentGroup in $Group) { + $cms = Find-CmsGroup -CmsGrp $cmsStore.DatabaseEngineServerGroup.ServerGroups -Stopat $currentGroup + if ($null -eq $cms) { + Write-Message -Level Output -Message "No groups found matching that name on instance '$instance'." + continue + } + $servers += ($cms.GetDescendantRegisteredServers()) + } + } + else { + $cms = $cmsStore.DatabaseEngineServerGroup + $servers += ($cms.GetDescendantRegisteredServers()) + } + if ($Group -and (Test-Bound -ParameterName Group -Not)) { + #add root ones + $servers += ($cmsstore.DatabaseEngineServerGroup.RegisteredServers) + } + + # Close the connection, otherwise using it with the ServersStore will keep it open + $cmsStore.ServerConnection.Disconnect() + } + + foreach ($server in $servers) { + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $null + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $null + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $null + + if ($ResolveNetworkName) { + try { + $lookup = Resolve-DbaNetworkName $server.ServerName -Turbo + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $lookup.ComputerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $lookup.FQDN + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $lookup.IPAddress + } + catch { + try { + $lookup = Resolve-DbaNetworkName $server.ServerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $lookup.ComputerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $lookup.FQDN + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $lookup.IPAddress + } + catch {} + } + } + + Add-Member -Force -InputObject $server -MemberType ScriptMethod -Name ToString -Value { $this.ServerName } + Select-DefaultView -InputObject $server -Property $defaults + } + + if ($IncludeSelf -and $servers) { + $self = $servers[0].PsObject.Copy() + $self | Add-Member -MemberType NoteProperty -Name Name -Value "CMS Instance" -Force + $self.ServerName = $instance + $self.Description = $null + $self.SecureConnectionString = $null + Select-DefaultView -InputObject $self -Property $defaults + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Parameter ExcludeCmsServer + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaRegisteredServerName + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-SqlRegisteredServerName + } +} +function Get-DbaRegisteredServerGroup { + <# + .SYNOPSIS + Gets list of Server Groups objects stored in SQL Server Central Management Server (CMS). + + .DESCRIPTION + Returns an array of Server Groups found in the CMS. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Group + Specifies one or more groups to include from SQL Server Central Management Server. + + .PARAMETER ExcludeGroup + Specifies one or more Central Management Server groups to exclude. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Tony Wilhelm (@tonywsql) + Tags: RegisteredServer, CMS + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServerGroup + + .EXAMPLE + Get-DbaRegisteredServerGroup -SqlInstance sqlserver2014a + + Gets the top level groups from the CMS on sqlserver2014a, using Windows Credentials. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -SqlCredential $credential + + Gets the top level groups from the CMS on sqlserver2014a, using SQL Authentication to authenticate to the server. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR, Accounting + + Gets the HR and Accounting groups from the CMS on sqlserver2014a. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR\Development + + Returns the sub-group Development of the HR group from the CMS on sqlserver2014a. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Group, + [object[]]$ExcludeGroup, + [switch]$EnableException + ) + begin { + function Find-CmsGroup { + [OutputType([object[]])] + [cmdletbinding()] + param( + $CmsGrp, + $Base = $null, + $Stopat + ) + $results = @() + + foreach ($el in $CmsGrp) { + if ($null -eq $Base -or [string]::IsNullOrWhiteSpace($Base) ) { + $partial = $el.name + } + else { + $partial = "$Base\$($el.name)" + } + if ($partial -eq $Stopat) { + return $el + } + else { + foreach ($elg in $el.ServerGroups) { + $results += Find-CmsGroup -CmsGrp $elg -Base $partial -Stopat $Stopat + } + } + } + return $results + } + } + + process { + + if (Test-FunctionInterrupt) { + return + } + + $groups = @() + foreach ($instance in $SqlInstance) { + + try { + $cmsStore = Get-DbaRegisteredServersStore -SqlInstance $instance -SqlCredential $SqlCredential -EnableException + } + catch { + Stop-Function -Message "Cannot access Central Management Server '$instance'." -ErrorRecord $_ -Continue + } + + if ($group) { + foreach ($currentGroup in $group) { + $cms = Find-CmsGroup -CmsGrp $cmsStore.DatabaseEngineServerGroup.ServerGroups -Stopat $currentGroup + if ($null -eq $cms) { + Write-Message -Level Output -Message "No groups found matching '$($currentGroup)' on instance '$instance'." + continue + } + $groups += $cms + } + } + else { + $groups = $cmsStore.DatabaseEngineServerGroup.ServerGroups + } + + if (Test-Bound -ParameterName ExcludeGroup) { + $groups = $groups | Where-Object Name -notin $ExcludeGroup + } + + # Close the connection, otherwise using it with the ServersStore will keep it open + $cmsStore.ServerConnection.Disconnect() + + $groups | Select-DefaultView -ExcludeProperty IsLocal, IsSystemServerGroup, IsDropped, Urn, Properties, Metadata, DuplicateFound, PropertyMetadataChanged, PropertyChanged + } + } +} +function Get-DbaRegisteredServersStore { + <# + .SYNOPSIS + Returns a SQL Server Registered Server Store Object + + .DESCRIPTION + Returns a SQL Server Registered Server Store object - useful for working with Central Management Store + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: RegisteredServer,CMS + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServersStore + + .EXAMPLE + Get-DbaRegisteredServersStore -SqlInstance sqlserver2014a + + Returns a SQL Server Registered Server Store Object from sqlserver2014a + + .EXAMPLE + Get-DbaRegisteredServersStore -SqlInstance sqlserver2014a -SqlCredential (Get-Credential sqladmin) + + Returns a SQL Server Registered Server Store Object from sqlserver2014a by logging in with the sqladmin login + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($Instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $store = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($server.ConnectionContext.SqlConnectionObject) + } + catch { + Stop-Function -Message "Cannot access Central Management Server on $instance." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $store -ExcludeProperty ServerConnection, DomainInstanceName, DomainName, Urn, Properties, Metadata, Parent, ConnectionContext, PropertyMetadataChanged, PropertyChanged + } + } +} +function Get-DbaResourceGovernorClassifierFunction { +<# +.SYNOPSIS +Gets the Resource Governor custom classifier Function + +.DESCRIPTION +Gets the Resource Governor custom classifier Function which is used for customize the workload groups usage + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER EnableException +By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. +This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. +Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Migration, ResourceGovernor +Author: Alessandro Alpi (@suxstellino), alessandroalpi.blog +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaResourceGovernorClassifierFunction + +.EXAMPLE +Get-DbaResourceGovernorClassifierFunction -SqlInstance sql2016 + +Gets the classifier function object of the SqlInstance sql2016 + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaResourceGovernorClassifierFunction + +Gets the classifier function object on Sql1 and Sql2/sqlexpress instances + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $classifierFunction = $null + + foreach ($currentFunction in $server.Databases["master"].UserDefinedFunctions) + { + $fullyQualifiedFunctionName = [string]::Format("[{0}].[{1}]", $currentFunction.Schema, $currentFunction.Name) + if ($fullyQualifiedFunctionName -eq $server.ResourceGovernor.ClassifierFunction) + { + $classifierFunction = $currentFunction + } + } + + if ($classifierFunction) { + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name Database -value 'master' + } + + Select-DefaultView -InputObject $classifierFunction -Property ComputerName, InstanceName, SqlInstance, Database, Schema, CreateDate, DateLastModified, Name, DataType + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaRestoreHistory { + <# + .SYNOPSIS + Returns restore history details for databases on a SQL Server. + + .DESCRIPTION + By default, this command will return the server name, database, username, restore type, date, from file and to files. + + Thanks to https://www.mssqltips.com/SqlInstancetip/1724/when-was-the-last-time-your-sql-server-database-was-restored/ for the query and https://sqlstudies.com/2016/07/27/when-was-this-database-restored/ for the idea. + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to operate on. Requires SQL Server 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER Since + Specifies a datetime to use as the starting point for searching backup history. + + .PARAMETER Force + Deprecated. + + .PARAMETER Last + If this switch is enabled, the last restore action performed on each database is returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRestoreHistory + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2016 + + Returns server name, database, username, restore type, date for all restored databases on sql2016. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2016 -Database db1, db2 -Since '7/1/2016 10:47:00' + + Returns restore information only for databases db1 and db2 on sql2016 since July 1, 2016 at 10:47 AM. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2014, sql2016 -Exclude db1 + + Lots of detailed information for all databases except db1 on sql2014 and sql2016. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2014 -Database AdventureWorks2014, pubs | Format-Table + + Adds From and To file information to output, returns information only for AdventureWorks2014 and pubs, and formats the data as a table. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2016 | Get-DbaRestoreHistory + + Returns database restore information for every database on every server listed in the Central Management Server on sql2016. + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$Since, + [switch]$Force, + [switch]$Last, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0.0" -EnableException:$false -Parameter 'Force' + + if ($Since -ne $null) { + $Since = $Since.ToString("yyyy-MM-ddTHH:mm:ss") + } + } + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + $computername = $server.NetName + $instancename = $server.ServiceName + $servername = $server.DomainInstanceName + + if ($force -eq $true) { + $select = "SELECT '$computername' AS [ComputerName], + '$instancename' AS [InstanceName], + '$servername' AS [SqlInstance], * " + } + else { + $select = "SELECT + '$computername' AS [ComputerName], + '$instancename' AS [InstanceName], + '$servername' AS [SqlInstance], + rsh.destination_database_name AS [Database], + --rsh.restore_history_id as RestoreHistoryID, + rsh.user_name AS [Username], + CASE + WHEN rsh.restore_type = 'D' THEN 'Database' + WHEN rsh.restore_type = 'F' THEN 'File' + WHEN rsh.restore_type = 'G' THEN 'Filegroup' + WHEN rsh.restore_type = 'I' THEN 'Differential' + WHEN rsh.restore_type = 'L' THEN 'Log' + WHEN rsh.restore_type = 'V' THEN 'Verifyonly' + WHEN rsh.restore_type = 'R' THEN 'Revert' + ELSE rsh.restore_type + END AS [RestoreType], + rsh.restore_date AS [Date], + ISNULL(STUFF((SELECT ', ' + bmf.physical_device_name + FROM msdb.dbo.backupmediafamily bmf + WHERE bmf.media_set_id = bs.media_set_id + FOR XML PATH('')), 1, 2, ''), '') AS [From], + ISNULL(STUFF((SELECT ', ' + rf.destination_phys_name + FROM msdb.dbo.restorefile rf + WHERE rsh.restore_history_id = rf.restore_history_id + FOR XML PATH('')), 1, 2, ''), '') AS [To], + bs.first_lsn, + bs.last_lsn, + bs.checkpoint_lsn, + bs.database_backup_lsn, + bs.backup_finish_date, + bs.backup_finish_date AS BackupFinishDate + " + } + + $from = " FROM msdb.dbo.restorehistory rsh + INNER JOIN msdb.dbo.backupset bs ON rsh.backup_set_id = bs.backup_set_id" + + if ($ExcludeDatabase -or $Database -or $Since -or $last) { + $where = " WHERE " + } + + $wherearray = @() + + if ($ExcludeDatabase) { + $dblist = $ExcludeDatabase -join "','" + $wherearray += " destination_database_name not in ('$dblist')" + } + + if ($Database) { + $dblist = $Database -join "','" + $wherearray += "destination_database_name in ('$dblist')" + } + + if ($null -ne $Since) { + $wherearray += "rsh.restore_date >= '$since'" + } + + + if ($last) { + $wherearray += "rsh.backup_set_id in + (select max(backup_set_id) from msdb.dbo.restorehistory + group by destination_database_name + )" + } + + if ($where.length -gt 0) { + $wherearray = $wherearray -join " and " + $where = "$where $wherearray" + } + + $sql = "$select $from $where" + + Write-Message -Level Debug -Message $sql + + $results = $server.ConnectionContext.ExecuteWithResults($sql).Tables.Rows + if ($last) { + $ga = $results | Group-Object Database + $tmpres = @() + foreach($g in $ga) { + $tmpres += $g.Group | Sort-Object -Property Date -Descending | Select-Object -First 1 + } + $results = $tmpres + } + $results | Select-DefaultView -ExcludeProperty first_lsn, last_lsn, checkpoint_lsn, database_backup_lsn, backup_finish_date + } + catch { + Stop-Function -Message "Failure" -Target $SqlInstance -Error $_ -Exception $_.Exception.InnerException -Continue + } + } + } +} +function Get-DbaRoleMember { + <# +.SYNOPSIS +Get members of all roles on a Sql instance. + +.DESCRIPTION +Get members of all roles on a Sql instance. + +Default output includes columns SQLServer, Database, Role, Member. + +.PARAMETER SQLInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER IncludeServerLevel +Shows also information on Server Level Permissions. + +.PARAMETER NoFixedRole +Excludes all members of fixed roles. + +.PARAMETER Credential +Credential object used to connect to the SQL Server as a different user. + +.NOTES +Tags: Role, Database, Security, Login +Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK + https://dbatools.io/Get-DbaRoleMember + +.EXAMPLE +Get-DbaRoleMember -SqlInstance ServerA + +Returns a custom object displaying SQLServer, Database, Role, Member for all DatabaseRoles. + +.EXAMPLE +Get-DbaRoleMember -SqlInstance sql2016 | Out-Gridview + +Returns a gridview displaying SQLServer, Database, Role, Member for all DatabaseRoles. + +.EXAMPLE +Get-DbaRoleMember -SqlInstance ServerA\sql987 -IncludeServerLevel + +Returns a gridview displaying SQLServer, Database, Role, Member for both ServerRoles and DatabaseRoles. + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('SqlServer', 'ServerInstance')] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeServerLevel, + [switch]$NoFixedRole + ) + + process { + + foreach ($instance in $sqlinstance) { + Write-Verbose "Connecting to $Instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Warning "Failed to connect to $instance" + continue + } + + if ($IncludeServerLevel) { + Write-Verbose "Server Role Members included" + $instroles = $null + Write-Verbose "Getting Server Roles on $instance" + $instroles = $server.roles + if ($NoFixedRole) { + $instroles = $instroles | Where-Object { $_.isfixedrole -eq $false } + } + ForEach ($instrole in $instroles) { + Write-Verbose "Getting Server Role Members for $instrole on $instance" + $irmembers = $null + $irmembers = $instrole.enumserverrolemembers() + ForEach ($irmem in $irmembers) { + [PSCustomObject]@{ + SQLInstance = $instance + Database = $null + Role = $instrole.name + Member = $irmem.tostring() + } + } + } + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Verbose "Checking accessibility of $db on $instance" + + if ($db.IsAccessible -ne $true) { + Write-Warning "Database $db on $instance is not accessible" + continue + } + + $dbroles = $db.roles + Write-Verbose "Getting Database Roles for $db on $instance" + + if ($NoFixedRole) { + $dbroles = $dbroles | Where-Object { $_.isfixedrole -eq $false } + } + + foreach ($dbrole in $dbroles) { + Write-Verbose "Getting Database Role Members for $dbrole in $db on $instance" + $dbmembers = $dbrole.enummembers() + ForEach ($dbmem in $dbmembers) { + [PSCustomObject]@{ + SqlInstance = $instance + Database = $db.name + Role = $dbrole.name + Member = $dbmem.tostring() + } + } + } + } + } + } +} +function Get-DbaRunningJob { + <# + .SYNOPSIS + Returns all non-idle Agent jobs running on the server. + + .DESCRIPTION + This function returns agent jobs that active on the SQL Server instance when calling the command. The information is gathered the SMO JobServer.jobs and be returned either in detailed or standard format. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Process, Session, ActivityMonitor, Agent, Job + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRunningJob + + .EXAMPLE + Get-DbaRunningJob -SqlInstance localhost + + Returns any active jobs on localhost. + + .EXAMPLE + Get-DbaRunningJob -SqlInstance localhost -Detailed + + Returns a detailed output of any active jobs on localhost. + + .EXAMPLE + 'localhost','localhost\namedinstance' | Get-DbaRunningJob + + Returns all active jobs on multiple instances piped into the function. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failed to connect to: $Server." -Target $server -ErrorRecord $_ -Continue + } + + $jobs = $server.JobServer.jobs | Where-Object { $_.CurrentRunStatus -ne 'Idle' } + + if (!$jobs) { + Write-Message -Level Verbose -Message "No Jobs are currently running on: $Server." + } + else { + foreach ($job in $jobs) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Name = $job.name + Category = $job.Category + CurrentRunStatus = $job.CurrentRunStatus + CurrentRunStep = $job.CurrentRunStep + HasSchedule = $job.HasSchedule + LastRunDate = $job.LastRunDate + LastRunOutcome = $job.LastRunOutcome + JobStep = $job.JobSteps + } + } + } + } + } +} +function Get-DbaSchemaChangeHistory { + <# + .SYNOPSIS + Gets DDL changes logged in the system trace. + + .DESCRIPTION + Queries the default system trace for any DDL changes in the specified timeframe + Only works with SQL 2005 and later, as the system trace didn't exist before then + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Since + A date from which DDL changes should be returned. Default is to start at the beggining of the current trace file + + .PARAMETER Object + The name of a SQL Server object you want to look for changes on + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Backup, Database + Author: Stuart Moore (@napalmgram - http://stuart-moore.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSchemaChangeHistory + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost + + Returns all DDL changes made in all databases on the SQL Server instance localhost since the system trace began + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made in all databases on the SQL Server instance localhost in the last 7 days + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Database Finance, Prod -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made in the Prod and Finance databases on the SQL Server instance localhost in the last 7 days + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Database Finance -Object AccountsTable -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made to the AccountsTable object in the Finance database on the SQL Server instance localhost in the last 7 days + + #> + + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [DbaDateTime]$Since, + [string[]]$Object, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($Server.Version.Major -le 8) { + Stop-Function -Message "This command doesn't support SQL Server 2000, sorry about that" + return + } + $TraceFileQuery = "select path from sys.traces where is_default = 1" + + $TraceFile = $server.Query($TraceFileQuery) | Select-Object Path + + $Databases = $server.Databases + + if ($Database) { $Databases = $Databases | Where-Object Name -in $database } + + if ($ExcludeDatabase) { $Databases = $Databases | Where-Object Name -notin $ExcludeDatabase } + + foreach ($db in $Databases) { + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "$db on $server is inaccessible" -Continue + } + + $sql = "select SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + tt.databasename as 'DatabaseName', + starttime as 'DateModified', + Sessionloginname as 'LoginName', + NTusername as 'UserName', + applicationname as 'ApplicationName', + case eventclass + When '46' Then 'Create' + when '47' Then 'Drop' + when '164' then 'Alter' + end as 'DDLOperation', + s.name+'.'+o.name as 'Object', + o.type_desc as 'ObjectType' + from + sys.objects o inner join + sys.schemas s on s.schema_id=o.schema_id + cross apply (select * from ::fn_trace_gettable('$($TraceFile.path)',default) where ObjectID=o.object_id ) tt + where tt.objecttype not in (21587) + and tt.DatabaseID=db_id() + and tt.EventSubClass=0" + + if ($null -ne $since) { + $sql = $sql + " and tt.StartTime>'$Since' " + } + if ($null -ne $object) { + $sql = $sql + " and o.name in ('$($object -join ''',''')') " + } + + $sql = $sql + " order by tt.StartTime asc" + Write-Message -Level Verbose -Message "Querying Database $db on $instance" + Write-Message -Level Debug -Message "SQL: $sql" + + $db.Query($sql) | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, DatabaseName, DateModified, LoginName, UserName, ApplicationName, DDLOperation, Object, ObjectType + } + } + } +} + +function Get-DbaServerAudit { + <# + .SYNOPSIS + Gets SQL Security Audit information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaServerAudit command gets SQL Security Audit information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Audit + Return only specific audits + + .PARAMETER ExcludeAudit + Exclude specific audits + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Audit, Security, SqlAudit + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerAudit + + .EXAMPLE + Get-DbaServerAudit -SqlInstance localhost + + Returns all Security Audits on the local default SQL Server instance + + .EXAMPLE + Get-DbaServerAudit -SqlInstance localhost, sql2016 + + Returns all Security Audits for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [string[]]$Audit, + [string[]]$ExcludeAudit, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $audits = $server.Audits + + if (Test-Bound -ParameterName Audit) { + $audits = $audits | Where-Object Name -in $Audit + } + if (Test-Bound -ParameterName ExcludeAudit) { + $audits = $audits | Where-Object Name -notin $ExcludeAudit + } + + foreach ($currentaudit in $audits) { + $directory = $currentaudit.FilePath.TrimEnd("\") + $filename = $currentaudit.FileName + $fullname = "$directory\$filename" + $remote = $fullname.Replace(":", "$") + $remote = "\\$($currentaudit.Parent.NetName)\$remote" + + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name ComputerName -value $currentaudit.Parent.NetName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name InstanceName -value $currentaudit.Parent.ServiceName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name SqlInstance -value $currentaudit.Parent.DomainInstanceName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name FullName -value $fullname + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name RemoteFullName -value $remote + + Select-DefaultView -InputObject $currentaudit -Property ComputerName, InstanceName, SqlInstance, Name, 'Enabled as IsEnabled', FullName + } + } + } +} +function Get-DbaServerAuditSpecification { + <# + .SYNOPSIS + Gets SQL Security Audit Specification information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaServerAuditSpecification command gets SQL Security Audit Specification information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Audit, Security, SqlAudit + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerAuditSpecification + + .EXAMPLE + Get-DbaServerAuditSpecification -SqlInstance localhost + + Returns all Security Audit Specifications on the local default SQL Server instance + + .EXAMPLE + Get-DbaServerAuditSpecification -SqlInstance localhost, sql2016 + + Returns all Security Audit Specifications for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Verbose "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.versionMajor -lt 10) { + Write-Warning "Server Audits are only supported in SQL Server 2008 and above. Quitting." + continue + } + + foreach ($auditSpecification in $server.ServerAuditSpecifications) { + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $auditSpecification -Property ComputerName, InstanceName, SqlInstance, ID, Name, AuditName, Enabled, CreateDate, DateLastModified, Guid + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlServerAuditSpecification + } +} +function Get-DbaServerInstallDate { + <# +.SYNOPSIS +Returns the install date of a SQL Instance and Windows Server, depending on what is passed. + +.DESCRIPTION +By default, this command returns for each SQL Instance instance passed in: +SQL Instance install date, formatted as a string +Hosting Windows server install date, formatted as a string + +.PARAMETER SqlInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER Credential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER IncludeWindows +Includes the Windows Server Install date information + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: CIM +Author: Mitchell Hamann (@SirCaptainMitch), mitchellhamann.com + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaServerInstallDate + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance SqlBox1\Instance2 + +Returns an object with SQL Instance Install date as a string and the Windows install date as string. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance winserver\sqlexpress, sql2016 + +Returns an object with SQL Instance Install date as a string and the Windows install date as a string for both SQLInstances that are passed to the cmdlet. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance sqlserver2014a, sql2016 + +Returns an object with only the SQL Server Install date as a string. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance sqlserver2014a, sql2016 -IncludeWindows + +Returns an object with the Windows Install date and the SQL install date as a string. + +.EXAMPLE +Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaServerInstallDate + +Returns an object with SQL Instance install date as a string for every server listed in the Central Management Server on sql2014 + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [PSCredential] + $Credential, + [Switch]$IncludeWindows, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -ge 9) { + Write-Message -Level Verbose -Message "Getting Install Date for: $instance" + $sql = "SELECT create_date FROM sys.server_principals WHERE sid = 0x010100000000000512000000" + [DbaDateTime]$sqlInstallDate = $server.Query($sql, 'master', $true).create_date + + } + else { + Write-Message -Level Verbose -Message "Getting Install Date for: $instance" + $sql = "SELECT schemadate FROM sysservers" + [DbaDateTime]$sqlInstallDate = $server.Query($sql, 'master', $true).create_date + } + + $WindowsServerName = $server.ComputerNamePhysicalNetBIOS + + if ($IncludeWindows) { + try { + [DbaDateTime]$windowsInstallDate = (Get-DbaCmObject -ClassName win32_OperatingSystem -ComputerName $WindowsServerName -Credential $Credential -EnableException).InstallDate + } + catch { + Stop-Function -Message "Failed to connect to: $WindowsServerName" -Continue -Target $instance -ErrorRecord $_ + } + } + + $object = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + SqlInstallDate = $sqlInstallDate + WindowsInstallDate = $windowsInstallDate + } + + if ($IncludeWindows) { + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, SqlInstallDate, WindowsInstallDate + } + else { + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, SqlInstallDate + } + + } + } +} +function Get-DbaServerProtocol { + <# + .SYNOPSIS + Gets the SQL Server related server protocols on a computer. + + .DESCRIPTION + Gets the SQL Server related server protocols on one or more computers. + + Requires Local Admin rights on destination computer(s). + The server protocols can be enabled and disabled when retrieved via WSMan. + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Protocol + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerProtocol + + .EXAMPLE + Get-DbaServerProtocol -ComputerName sqlserver2014a + + Gets the SQL Server related server protocols on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaServerProtocol + + Gets the SQL Server related server protocols on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaServerProtocol -ComputerName sql1,sql2 | Out-Gridview + + Gets the SQL Server related server protocols on computers sql1 and sql2, and shows them in a grid view. + + .EXAMPLE + (Get-DbaServerProtocol -ComputerName sql1 | Where { $_.DisplayName = 'via' }).Disable() + + Disables the VIA ServerNetworkProtocol on computer sql1. + If successful, returncode 0 is shown. + +#> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($Computer in $ComputerName.ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ($Server.FullComputerName) { + $Computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Getting SQL Server namespace on $computer" + $namespace = Get-DbaCmObject -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -Query "Select * FROM __NAMESPACE WHERE Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | + Where-Object { (Get-DbaCmObject -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue).count -gt 0 } | + Sort-Object Name -Descending | Select-Object -First 1 + if ($namespace.Name) { + Write-Message -Level Verbose -Message "Getting Cim class ServerNetworkProtocol in Namespace $($namespace.Name) on $Computer" + try { + $prot = Get-DbaCmObject -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue + $prot | Add-Member -Force -MemberType ScriptMethod -Name Enable -Value { Invoke-CimMethod -MethodName SetEnable -InputObject $this } + $prot | Add-Member -Force -MemberType ScriptMethod -Name Disable -Value { Invoke-CimMethod -MethodName SetDisable -InputObject $this } + foreach ($protocol in $prot) { Select-DefaultView -InputObject $protocol -Property 'PSComputerName as ComputerName', 'InstanceName', 'ProtocolDisplayName as DisplayName', 'ProtocolName as Name', 'MultiIpconfigurationSupport as MultiIP', 'Enabled as IsEnabled' } + } + catch { + Write-Message -Level Warning -Message "No Sql ServerNetworkProtocol found on $Computer" + } + } + else { + Write-Message -Level Warning -Message "No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." + } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + } +} +function Get-DbaServerRole { + <# + .SYNOPSIS + Gets the list of server-level roles. + + .DESCRIPTION + Gets the list of server-level roles for SQL Server instance. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ServerRole + Server-Level role to filter results to that role only. + + .PARAMETER ExcludeServerRole + Server-Level role to exclude from results. + + .PARAMETER ExcludeFixedRole + Filter the fixed server-level roles. Only applies to SQL Server 2017 that supports creation of server-level roles. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ServerRole, Security + Original Author: Shawn Melton (@wsmelton) + + Website: https: //dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerRole + + .EXAMPLE + Get-DbaServerRole -SqlInstance sql2016a + + Outputs list of server-level roles for sql2016a instance. + + .EXAMPLE + Get-DbaServerRole -SqlInstance sql2017a -ExcludeFixedRole + + Outputs the server-level role(s) that are not fixed roles on sql2017a instance. + #> + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$ServerRole, + [object[]]$ExcludeServerRole, + [switch]$ExcludeFixedRole, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $roles = $server.Roles + + if ($ServerRole) { + $roles = $roles | Where-Object Name -In $ServerRole + } + if ($ExcludeServerRole) { + $roles = $roles | Where-Object Name -NotIn $ExcludeServerRole + } + if ($ExcludeFixedRole) { + $roles = $roles | Where-Object IsFixedRole -eq $false + } + + foreach ($role in $roles) { + $members = $role.EnumMemberNames() + + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name Login -Value $members + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + $default = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name as Role', 'IsFixedRole', 'DateCreated', 'DateModified' + Select-DefaultView -InputObject $role -Property $default + } + } + } +} +function Get-DbaSpConfigure { + <# + .SYNOPSIS + Returns all server level system configuration (sys.configuration/sp_configure) information + + .DESCRIPTION + This function returns server level system configuration (sys.configuration/sp_configure) information. The information is gathered through SMO Configuration.Properties. + The data includes the default value for each configuration, for quick identification of values that may have been changed. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a + collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + Return only specific configurations -- auto-populated from source server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SpConfig, Configure, Configuration + Author: Nic Cain, https://sirsql.net/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSpConfigure + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance localhost + + Returns server level configuration data on the localhost (ServerName, Name, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) + + .EXAMPLE + 'localhost','localhost\namedinstance' | Get-DbaSpConfigure + + Returns system configuration information on multiple instances piped into the function + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance localhost + + Returns server level configuration data on the localhost (ServerName, Name, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance sql2012 -Name MaxServerMemory + + Returns only the system configuration for MaxServerMemory. Configs is auto-populated for tabbing convenience. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Config", "ConfigName")] + [string[]]$Name, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Warning "Failed to connect to: $instance" + continue + } + + #Get a list of the configuration property parents, and exclude the Parent, Properties values + $proplist = Get-Member -InputObject $server.Configuration -MemberType Property -Force | Select-Object Name | Where-Object { $_.Name -ne "Parent" -and $_.Name -ne "Properties" } + + if ($Name) { + $proplist = $proplist | Where-Object { $_.Name -in $Name } + } + + #Grab the default sp_configure property values from the external function + $defaultConfigs = (Get-SqlDefaultSpConfigure -SqlVersion $server.VersionMajor).psobject.properties; + + #Iterate through the properties to get the configuration settings + foreach ($prop in $proplist) { + $propInfo = $server.Configuration.$($prop.Name) + $defaultConfig = $defaultConfigs | Where-Object { $_.Name -eq $propInfo.DisplayName }; + + if ($defaultConfig.Value -eq $propInfo.RunValue) { $isDefault = $true } + else { $isDefault = $false } + + #Ignores properties that are not valid on this version of SQL + if (!([string]::IsNullOrEmpty($propInfo.RunValue))) { + # some displaynames were empty + $displayname = $propInfo.DisplayName + if ($displayname.Length -eq 0) { $displayname = $prop.Name } + + [pscustomobject]@{ + ServerName = $server.Name + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Name = $prop.Name + DisplayName = $displayname + Description = $propInfo.Description + IsAdvanced = $propInfo.IsAdvanced + IsDynamic = $propInfo.IsDynamic + MinValue = $propInfo.Minimum + MaxValue = $propInfo.Maximum + ConfiguredValue = $propInfo.ConfigValue + RunningValue = $propInfo.RunValue + DefaultValue = $defaultConfig.Value + IsRunningDefaultValue = $isDefault + Parent = $server + ConfigName = $prop.Name + } | Select-DefaultView -ExcludeProperty ServerName, Parent, ConfigName + } + } + } + } +} +#ValidationTags#FlowControl,Pipeline# +function Get-DbaSpn { + <# + .SYNOPSIS + Returns a list of set service principal names for a given computer/AD account + + .DESCRIPTION + Get a list of set SPNs. SPNs are set at the AD account level. You can either retrieve set SPNs for a computer, or any SPNs set for + a given active directory account. You can query one, or both. You'll get a list of every SPN found for either search term. + + .PARAMETER ComputerName + The servers you want to return set SPNs for. This is defaulted automatically to localhost. + + .PARAMETER AccountName + The accounts you want to retrieve set SPNs for. + + .PARAMETER Credential + User credential to connect to the remote servers or active directory. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SPN + Author: Drew Furgiuele (@pittfurg), http://www.port1433.com + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSpn + + .EXAMPLE + Get-DbaSpn -ServerName SQLSERVERA -Credential (Get-Credential) + + Returns a custom object with SearchTerm (ServerName) and the SPNs that were found + + .EXAMPLE + Get-DbaSpn -AccountName domain\account -Credential (Get-Credential) + + Returns a custom object with SearchTerm (domain account) and the SPNs that were found + + .EXAMPLE + Get-DbaSpn -ServerName SQLSERVERA,SQLSERVERB -Credential (Get-Credential) + + Returns a custom object with SearchTerm (ServerName) and the SPNs that were found for multiple computers + #> + [cmdletbinding()] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [string[]]$ComputerName, + [Parameter(Mandatory = $false)] + [string[]]$AccountName, + [Parameter(Mandatory = $false)] + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Function Process-Account ($AccountName) { + + ForEach ($account in $AccountName) { + Write-Message -Message "Looking for account $account..." -Level Verbose + $searchfor = 'User' + if ($account.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $account -Type $searchfor -Credential $Credential -EnableException + } + catch { + Write-Message -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($Account)." -Level Warning + continue + } + if ($Result.Count -gt 0) { + try { + $results = $Result.GetUnderlyingObject() + $spns = $results.Properties.servicePrincipalName + } + catch { + Write-Message -Message "The SQL Service account ($Account) has been found, but you don't have enough permission to inspect its SPNs" -Level Warning + continue + } + } + else { + Write-Message -Message "The SQL Service account ($Account) has not been found" -Level Warning + continue + } + + foreach ($spn in $spns) { + if ($spn -match "\:") { + try { + $port = [int]($spn -Split "\:")[1] + } + catch { + $port = $null + } + if ($spn -match "\/") { + $serviceclass = ($spn -Split "\/")[0] + } + } + [pscustomobject] @{ + Input = $Account + AccountName = $Account + ServiceClass = "MSSQLSvc" # $serviceclass + Port = $port + SPN = $spn + } + } + } + } + if ($ComputerName.Count -eq 0 -and $AccountName.Count -eq 0) { + $ComputerName = @($env:COMPUTERNAME) + } + } + + process { + + foreach ($computer in $ComputerName) { + if ($computer) { + if ($computer.EndsWith('$')) { + Write-Message -Message "$computer is an account name. Processing as account." -Level Verbose + Process-Account -AccountName $computer + continue + } + } + + Write-Message -Message "Getting SQL Server SPN for $computer" -Level Verbose + $spns = Test-DbaSpn -ComputerName $computer -Credential $Credential + + $sqlspns = 0 + $spncount = $spns.count + Write-Message -Message "Calculated $spncount SQL SPN entries that should exist for $computer" -Level Verbose + foreach ($spn in $spns | Where-Object { $_.IsSet -eq $true }) { + $sqlspns++ + + if ($accountName) { + if ($accountName -eq $spn.InstanceServiceAccount) { + [pscustomobject] @{ + Input = $computer + AccountName = $spn.InstanceServiceAccount + ServiceClass = "MSSQLSvc" + Port = $spn.Port + SPN = $spn.RequiredSPN + } + } + } + else { + [pscustomobject] @{ + Input = $computer + AccountName = $spn.InstanceServiceAccount + ServiceClass = "MSSQLSvc" + Port = $spn.Port + SPN = $spn.RequiredSPN + } + } + } + Write-Message -Message "Found $sqlspns set SQL SPN entries for $computer" -Level Verbose + } + + if ($AccountName) { + foreach ($account in $AccountName) { + Process-Account -AccountName $account + } + } + } +} +function Get-DbaSqlBuildReference { + <# + .SYNOPSIS + Returns SQL Server Build infos on a SQL instance + + .DESCRIPTION + Returns info about the specific build of a SQL instance, including the SP, the CU and the reference KB, wherever possible. + It also includes End Of Support dates as specified on Microsoft Lifecycle Policy + + .PARAMETER Build + Instead of connecting to a real instance, pass a string identifying the build to get the info back. + + .PARAMETER SqlInstance + Target any number of instances, in order to return their build state. + + .PARAMETER SqlCredential + When connecting to an instance, use the credentials specified. + + .PARAMETER Update + Looks online for the most up to date reference, replacing the local one. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.00.4502" + + Returns information about a build identified by "12.00.4502" (which is SQL 2014 with SP1 and CU11) + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.00.4502" -Update + + Returns information about a build trying to fetch the most up to date index online. When the online version is newer, the local one gets overwritten + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.0.4502","10.50.4260" + + Returns information builds identified by these versions strings + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a | Get-DbaSqlBuildReference + + Integrate with other commandlets to have builds checked for all your registered servers on sqlserver2014a + + .NOTES + Author: niphlod + Editor: Fred + Tags: SqlBuild + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlBuildReference +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding()] + Param ( + [version[]] + $Build, + + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance, + + [Alias("Credential")] + [PsCredential] + $SqlCredential, + + [switch] + $Update, + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + #region Helper functions + function Get-DbaSqlBuildReferenceIndex { + [CmdletBinding()] + Param ( + [string] + $Moduledirectory, + + [bool] + $Update, + + [bool] + $EnableException + ) + + $orig_idxfile = "$Moduledirectory\bin\dbatools-buildref-index.json" + $DbatoolsData = Get-DbaConfigValue -Name 'Path.DbatoolsData' + $writable_idxfile = Join-Path $DbatoolsData "dbatools-buildref-index.json" + + if (-not (Test-Path $orig_idxfile)) { + Write-Message -Level Warning -EnableException $EnableException -Message "Unable to read local SQL build reference file. Check your module integrity!" + } + + if ((-not (Test-Path $orig_idxfile)) -and (-not (Test-Path $writable_idxfile))) { + throw "Build reference file not found, check module health!" + } + + # If no writable copy exists, create one and return the module original + if (-not (Test-Path $writable_idxfile)) { + Copy-Item -Path $orig_idxfile -Destination $writable_idxfile -Force -ErrorAction Stop + $result = Get-Content $orig_idxfile -Raw | ConvertFrom-Json + } + + # Else, if both exist, update the writeable if necessary and return the current version + elseif (Test-Path $orig_idxfile) { + $module_content = Get-Content $orig_idxfile -Raw | ConvertFrom-Json + $data_content = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + + $module_time = Get-Date $module_content.LastUpdated + $data_time = Get-Date $data_content.LastUpdated + + $offline_time = $module_time + if ($module_time -gt $data_time) { + Copy-Item -Path $orig_idxfile -Destination $writable_idxfile -Force -ErrorAction Stop + $result = $module_content + } + else { + $result = $data_content + $offline_time = $data_time + } + # If Update is passed, try to fetch from online resource and store into the writeable + if ($Update) { + $WebContent = Get-DbaSqlBuildReferenceIndexOnline -EnableException $EnableException + if ($null -ne $WebContent) { + $webdata_content = $WebContent.Content | ConvertFrom-Json + $webdata_time = Get-Date $webdata_content.LastUpdated + if ($webdata_time -gt $offline_time) { + Write-Message -Level Output -Message "Index updated correctly, last update on: $(Get-Date -Date $webdata_time -Format s), was $(Get-Date -Date $offline_time -Format s)" + $WebContent.Content | Out-File $writable_idxfile -Encoding utf8 -ErrorAction Stop + $result = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + } + } + } + } + + # Else if the module version of the file no longer exists, but the writable version exists, return the writable version + else { + $result = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + } + + $LastUpdated = Get-Date -Date $result.LastUpdated + if ($LastUpdated -lt (Get-Date).AddDays(-45)) { + Write-Message -Level Warning -EnableException $EnableException -Message "Index is stale, last update on: $(Get-Date -Date $LastUpdated -Format s), try the -Update parameter to fetch the most up to date index" + } + + $result.Data | Select-Object @{ Name = "VersionObject"; Expression = { [version]$_.Version } }, * + } + + function Get-DbaSqlBuildReferenceIndexOnline { + [CmdletBinding()] + Param ( + [bool] + $EnableException + ) + $url = Get-DbaConfigValue -Name 'assets.sqlbuildreference' + try { + $WebContent = Invoke-WebRequest $url -ErrorAction Stop + } + catch { + try { + Write-Message -Level Verbose -EnableException $EnableException -Message "Probably using a proxy for internet access, trying default proxy settings" + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + $WebContent = Invoke-WebRequest $url -ErrorAction Stop + } + catch { + Write-Message -Level Warning -EnableException $EnableException -Message "Couldn't download updated index from $url" + return + } + } + return $WebContent + } + + function Resolve-DbaSqlBuild { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [version] + $Build, + + $Data, + + [bool] + $EnableException + ) + + Write-Message -Level Verbose -EnableException $EnableException -Message "Looking for $Build" + + $IdxVersion = $Data | Where-Object Version -like "$($Build.Major).$($Build.Minor).*" + $Detected = @{ } + $Detected.MatchType = 'Approximate' + Write-Message -Level Verbose -EnableException $EnableException -Message "We have $($IdxVersion.Length) builds in store for this Release" + If ($IdxVersion.Length -eq 0) { + Write-Message -Level Warning -EnableException $EnableException -Message "No info in store for this Release" + $Detected.Warning = "No info in store for this Release" + } + else { + $LastVer = $IdxVersion[0] + } + foreach ($el in $IdxVersion) { + if ($null -ne $el.Name) { + $Detected.Name = $el.Name + } + if ($el.VersionObject -gt $Build) { + $Detected.MatchType = 'Approximate' + $Detected.Warning = "$Build not found, closest build we have is $($LastVer.Version)" + break + } + $LastVer = $el + if ($null -ne $el.SP) { + $Detected.SP = $el.SP + $Detected.CU = $null + } + if ($null -ne $el.CU) { + $Detected.CU = $el.CU + } + if ($null -ne $el.SupportedUntil) { + $Detected.SupportedUntil = (Get-Date -date $el.SupportedUntil) + } + $Detected.KB = $el.KBList + if ($el.Version -eq $Build) { + $Detected.MatchType = 'Exact' + break + } + } + return $Detected + } + #endregion Helper functions + + $moduledirectory = $MyInvocation.MyCommand.Module.ModuleBase + + try { + $IdxRef = Get-DbaSqlBuildReferenceIndex -Moduledirectory $moduledirectory -Update $Update -EnableException $EnableException + } + catch { + Stop-Function -Message "Error loading SQL build reference" -ErrorRecord $_ + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + #region Ensure the connection is established + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + + try { + $null = $server.Version.ToString() + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + #endregion Ensure the connection is established + + $Detected = Resolve-DbaSqlBuild -Build $server.Version -Data $IdxRef -EnableException $EnableException + + [PSCustomObject]@{ + SqlInstance = $server.DomainInstanceName + Build = $server.Version + NameLevel = $Detected.Name + SPLevel = $Detected.SP + CULevel = $Detected.CU + KBLevel = $Detected.KB + SupportedUntil = $Detected.SupportedUntil + MatchType = $Detected.MatchType + Warning = $Detected.Warning + } + } + + foreach ($buildstr in $Build) { + $Detected = Resolve-DbaSqlBuild -Build $buildstr -Data $IdxRef -EnableException $EnableException + + [PSCustomObject]@{ + SqlInstance = $null + Build = $buildstr + NameLevel = $Detected.Name + SPLevel = $Detected.SP + CULevel = $Detected.CU + KBLevel = $Detected.KB + SupportedUntil = $Detected.SupportedUntil + MatchType = $Detected.MatchType + Warning = $Detected.Warning + } | Select-DefaultView -ExcludeProperty SqlInstance + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaSqlFeature { + <# + .SYNOPSIS + Runs the SQL Server feature discovery report (setup.exe /Action=RunDiscovery) + + .DESCRIPTION + Runs the SQL Server feature discovery report (setup.exe /Action=RunDiscovery) + + Inspired by Dave Mason's (@BeginTry) post at + https://itsalljustelectrons.blogspot.be/2018/04/SQL-Server-Discovery-Report.html + + Assumptions: + 1. The sub-folder "Microsoft SQL Server" exists in $env:ProgramFiles, + even if SQL was installed to a non-default path. This has been + verified on SQL 2008R2 and SQL 2012. Further verification may be needed. + 2. The discovery report displays installed components for the version of SQL + Server associated with setup.exe, along with installed components of all + lesser versions of SQL Server that are installed. + + .PARAMETER ComputerName + The target computer. If the target is not localhost, it must have PowerShell remoting enabled. + + Note that this is not the SqlInstance, but rather the ComputerName + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Feature, Component + Author: Chrissy LeMaire (@cl) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlFeature + + .EXAMPLE + Get-DbaSqlFeature -ComputerName sql2017, sql2016, sql2005 + + Gets all SQL Server features for all instances on sql2017, sql2016 and sql2005. + + .EXAMPLE + Get-DbaSqlFeature -Verbose + + Gets all SQL Server features for all instances on localhost. Outputs to screen if no instances are found. + + .EXAMPLE + Get-DbaSqlFeature -ComputerName sql2017 -Credential (Get-Credential ad\sqladmin) + + Gets all SQL Server features for all instances on sql2017 using the ad\sqladmin credential (which has access to the Windows Server). + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch]$EnableException + ) + + begin { + $scriptblock = { + $setup = Get-ChildItem -Recurse -Include setup.exe -Path "$env:ProgramFiles\Microsoft SQL Server" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match 'Setup Bootstrap\\SQL' -or $_.FullName -match 'Bootstrap\\Release\\Setup.exe' -or $_.FullName -match 'Bootstrap\\Setup.exe' } | + Sort-Object FullName -Descending | Select-Object -First 1 + if ($setup) { + $null = Start-Process -FilePath $setup.FullName -ArgumentList "/Action=RunDiscovery /q" -Wait + $parent = Split-Path (Split-Path $setup.Fullname) + $xmlfile = Get-ChildItem -Recurse -Include SqlDiscoveryReport.xml -Path $parent | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + if ($xmlfile) { + $xml = [xml](Get-Content -Path $xmlfile) + $xml.ArrayOfDiscoveryInformation.DiscoveryInformation + } + } + } + } + + process { + foreach ($computer in $ComputerName) { + try { + $results = Invoke-Command2 -ComputerName $Computer -ScriptBlock $scriptblock -Credential $Credential -Raw + + if (-not $results) { + Write-Message -Level Verbose -Message "No features found on $computer" + } + + foreach ($result in $results) { + [pscustomobject]@{ + ComputerName = $computer + Product = $result.Product + Instance = $result.Instance + InstanceID = $result.InstanceID + Feature = $result.Feature + Language = $result.Language + Edition = $result.Edition + Version = $result.Version + Clustered = $result.Clustered + Configured = $result.Configured + } + } + } + catch { + Stop-Function -Continue -ErrorRecord $_ -Message "Failure" + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaSqlInstanceProperty { + <# + .SYNOPSIS + Gets SQL Server instance properties of one or more instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaSqlInstanceProperty command gets SQL Server instance properties from the SMO object sqlserver. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER InstanceProperty + SQL Server instance property(ies) to include. + + .PARAMETER ExcludeInstanceProperty + SQL Server instance property(ies) to exclude. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, Configure, Configuration + Author: Klaas Vandenberghe (@powerdbaklaas) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlInstanceProperty + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance localhost + + Returns SQL Server instance properties on the local default SQL Server instance + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2, sql4\sqlexpress + + Returns SQL Server instance properties on default instance on sql2 and sqlexpress instance on sql4 + + .EXAMPLE + 'sql2','sql4' | Get-DbaSqlInstanceProperty + + Returns SQL Server instance properties on sql2 and sql4 + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2,sql4 -InstanceProperty DefaultFile + + Returns SQL Server instance property DefaultFile on instance sql2 and sql4 + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2,sql4 -ExcludeInstanceProperty DefaultFile + + Returns all SQL Server instance properties except DefaultFile on instance sql2 and sql4 + + .EXAMPLE + $cred = Get-Credential sqladmin + Get-DbaSqlInstanceProperty -SqlInstance sql2 -SqlCredential $cred + + Connects using sqladmin credential and returns SQL Server instance properties from sql2 + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$InstanceProperty, + [object[]]$ExcludeInstanceProperty, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $infoProperties = $server.Information.Properties + + if ($InstanceProperty) { + $infoProperties = $infoProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $infoProperties = $infoProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $infoProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'Information' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering information properties for $instance." -Target $instance -ErrorRecord $_ -Continue + } + + try { + $userProperties = $server.UserOptions.Properties + + if ($InstanceProperty) { + $userProperties = $userProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $userProperties = $userProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $userProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'UserOption' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering user options for $instance." -Target $instance -ErrorRecord $_ -Continue + } + + try { + $settingProperties = $server.Settings.Properties + + if ($InstanceProperty) { + $settingProperties = $settingProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $settingProperties = $settingProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $settingProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'Setting' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering settings for $instance." -Target $instance -ErrorRecord $_ -Continue + } + } + } +} +function Get-DbaSqlInstanceUserOption { + <# + .SYNOPSIS + Gets SQL Instance user options of one or more instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaSqlInstanceUserOption command gets SQL Instance user options from the SMO object sqlserver. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, Configure, UserOption + Author: Klaas Vandenberghe (@powerdbaklaas) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlInstanceUserOption + + .EXAMPLE + Get-DbaSqlInstanceUserOption -SqlInstance localhost + + Returns SQL Instance user options on the local default SQL Server instance + + .EXAMPLE + Get-DbaSqlInstanceUserOption -SqlInstance sql2, sql4\sqlexpress + + Returns SQL Instance user options on default instance on sql2 and sqlexpress instance on sql4 + + .EXAMPLE + 'sql2','sql4' | Get-DbaSqlInstanceUserOption + + Returns SQL Instance user options on sql2 and sql4 + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $props = $server.useroptions.properties + foreach ($prop in $props) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value + } + } + } +} +function Get-DbaSqlManagementObject { + <# + .SYNOPSIS + Gets SQL Mangaement Object versions installed on the machine. + + .DESCRIPTION + The Get-DbaSqlManagementObject returns an object with the Version and the + Add-Type Load Template for each version on the server. + + .PARAMETER ComputerName + The name of the target you would like to check + + .PARAMETER Credential + This command uses Windows credentials. This parameter allows you to connect remotely as a different user. + + .PARAMETER VersionNumber + This is the specific version number you are looking for. The function will look + for that version only. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SMO + Author: Ben Miller (@DBAduck - http://dbaduck.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlManagementObject + + .EXAMPLE + Get-DbaSqlManagementObject + + Returns all versions of SMO on the computer + + .EXAMPLE + Get-DbaSqlManagementObject -VersionNumber 13 + + Returns just the version specified. If the version does not exist then it will return nothing. + + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] + $Credential, + [int]$VersionNumber, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (!$VersionNumber) { + $VersionNumber = 0 + } + $scriptblock = { + $VersionNumber = [int]$args[0] + + Write-Message -Level Verbose -Message "Checking currently loaded SMO version" + $loadedversion = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.Fullname -like "Microsoft.SqlServer.SMO,*" } + if ($loadedversion) { + $loadedversion = $loadedversion | ForEach-Object { + if ($_.Location -match "__") { + ((Split-Path (Split-Path $_.Location) -Leaf) -split "__")[0] + } + else { + ((Get-ChildItem -Path $_.Location).VersionInfo.ProductVersion) + } + } + } + + Write-Message -Level Verbose -Message "Looking for included smo library" + $localversion = [version](Get-ChildItem -Path "$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll").VersionInfo.ProductVersion + + foreach ($version in $localversion) { + if ($VersionNumber -eq 0) { + Write-Message -Level Verbose -Message "Did not pass a version" + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $localversion + Loaded = $loadedversion -contains $localversion + LoadTemplate = "Add-Type -Path $("$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll")" + } + } + else { + Write-Message -Level Verbose -Message "Passed version $VersionNumber, looking for that specific version" + if ($localversion.ToString().StartsWith("$VersionNumber.")) { + Write-Message -Level Verbose -Message "Found the Version $VersionNumber" + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $localversion + Loaded = $loadedversion -contains $localversion + LoadTemplate = "Add-Type -Path $("$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll")" + } + } + } + } + + Write-Message -Level Verbose -Message "Looking for SMO in the Global Assembly Cache" + $smolist = (Get-ChildItem -Path "$env:SystemRoot\assembly\GAC_MSIL\Microsoft.SqlServer.Smo" | Sort-Object Name -Descending).Name + + foreach ($version in $smolist) { + $array = $version.Split("__") + if ($VersionNumber -eq 0) { + Write-Message -Level Verbose -Message "Did not pass a version, looking for all versions" + $currentversion = $array[0] + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $currentversion + Loaded = $loadedversion -contains $currentversion + LoadTemplate = "Add-Type -AssemblyName `"Microsoft.SqlServer.Smo, Version=$($array[0]), Culture=neutral, PublicKeyToken=89845dcd8080cc91`"" + } + } + else { + Write-Message -Level Verbose -Message "Passed version $VersionNumber, looking for that specific version" + if ($array[0].StartsWith("$VersionNumber.")) { + Write-Message -Level Verbose -Message "Found the Version $VersionNumber" + $currentversion = $array[0] + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $currentversion + Loaded = $loadedversion -contains $currentversion + LoadTemplate = "Add-Type -AssemblyName `"Microsoft.SqlServer.Smo, Version=$($array[0]), Culture=neutral, PublicKeyToken=89845dcd8080cc91`"" + } + } + } + } + } + } + + process { + foreach ($computer in $ComputerName.ComputerName) { + try { + Invoke-Command2 -ComputerName $computer -ScriptBlock $scriptblock -Credential $Credential -ArgumentList $VersionNumber -ErrorAction Stop + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $ComputerName + } + } + } +} +function Get-DbaSqlModule { + <# + .SYNOPSIS + Displays all objects in sys.sys_modules after specified modification date. Works on SQL Server 2008 and above. + + .DESCRIPTION + Quickly find modules (Stored Procs, Functions, Views, Constraints, Rules, Triggers, etc) that have been modified in a database, or across all databases. + Results will exclude the module definition, but can be queried explicitly. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. + + .PARAMETER ModifiedSince + DateTime value to use as minimum modified date of module. + + .PARAMETER Type + Limit by specific type of module. Valid choices include: View, TableValuedFunction, DefaultConstraint, StoredProcedure, Rule, InlineTableValuedFunction, Trigger, ScalarFunction + + .PARAMETER NoSystemDb + Allows you to suppress output on system databases + + .PARAMETER NoSystemObjects + Allows you to suppress output on system objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Brandon Abshire, netnerds.net + Tags: StoredProcedure, Trigger + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlModule + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008, sqlserver2012 + Return all modules for servers sql2008 and sqlserver2012 sorted by Database, Modify_Date ASC. + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008, sqlserver2012 | Select * + Shows hidden definition column (informative wall of text). + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008 -Database TestDB -ModifiedSince "01/01/2017 10:00:00 AM" + Return all modules on server sql2008 for only the TestDB database with a modified date after 01/01/2017 10:00:00 AM. + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008 -Type View, Trigger, ScalarFunction + Return all modules on server sql2008 for all databases that are triggers, views or scalar functions. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$ModifiedSince = "01/01/1900", + [ValidateSet("View", "TableValuedFunction", "DefaultConstraint", "StoredProcedure", "Rule", "InlineTableValuedFunction", "Trigger", "ScalarFunction")] + [string[]]$Type, + [switch]$NoSystemDb, + [switch]$NoSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $types = @() + + foreach ($t in $type) { + if ($t -eq "View") { $types += "VIEW" } + if ($t -eq "TableValuedFunction") { $types += "SQL_TABLE_VALUED_FUNCTION" } + if ($t -eq "DefaultConstraint") { $types += "DEFAULT_CONSTRAINT" } + if ($t -eq "StoredProcedure") { $types += "SQL_STORED_PROCEDURE" } + if ($t -eq "Rule") { $types += "RULE" } + if ($t -eq "InlineTableValuedFunction") { $types += "SQL_INLINE_TABLE_VALUED_FUNCTION" } + if ($t -eq "Trigger") { $types += "SQL_TRIGGER" } + if ($t -eq "ScalarFunction") { $types += "SQL_SCALAR_FUNCTION" } + } + + + $sql = "SELECT DB_NAME() AS DatabaseName, + so.name AS ModuleName, + so.object_id , + SCHEMA_NAME(so.schema_id) AS SchemaName , + so.parent_object_id , + so.type , + so.type_desc , + so.create_date , + so.modify_date , + so.is_ms_shipped , + sm.definition, + OBJECTPROPERTY(so.object_id, 'ExecIsStartUp') as startup + FROM sys.sql_modules sm + LEFT JOIN sys.objects so ON sm.object_id = so.object_id + WHERE so.modify_date >= '$($ModifiedSince)'" + if ($NoSystemObjects) { + $sql += "`n AND so.is_ms_shipped = 0" + } + if ($Type) { + $sqltypes = $types -join "','" + $sql += " AND type_desc in ('$sqltypes')" + } + $sql += "`n ORDER BY so.modify_date" + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = Get-DbaDatabase -SqlInstance $server + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + + foreach ($db in $databases) { + + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "The database $db is not accessible. Skipping database." -Target $db -Continue + } + + foreach ($row in $server.Query($sql, $db.name)) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DatabaseName + Name = $row.ModuleName + ObjectID = $row.object_id + SchemaName = $row.SchemaName + Type = $row.type_desc + CreateDate = $row.create_date + ModifyDate = $row.modify_date + IsMsShipped = $row.is_ms_shipped + ExecIsStartUp = $row.startup + Definition = $row.definition + } | Select-DefaultView -ExcludeProperty Definition + } + } + } + } +} +function Get-DbaSqlProductKey { + <# +.SYNOPSIS +Gets SQL Server Product Keys from local or destination SQL Servers. Works with SQL Server 2005-2016 + +.DESCRIPTION +Using a string of servers, a text file, or Central Management Server to provide a list of servers, this script will go to each server and get the product key for all installed instances. Clustered instances are supported as well. Requires regular user access to the SQL instances, SMO installed locally, Remote Registry enabled and accessible by the account running the script. + +Uses key decoder by Jakob Bindslet (http://goo.gl/1jiwcB) + +.PARAMETER SqlInstances +A comma separated list of servers. This can be the NetBIOS name, IP, or SQL instance name + +.PARAMETER SqlCms +Compiles list of servers to inventory using all servers stored within a Central Management Server. Requires having SQL Management Studio installed. + +.PARAMETER ServersFromFile +Uses a text file as input. The file must be formatted as such: +sqlserver1 +sqlserver2 + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.NOTES +Author: Chrissy LeMaire (@cl), netnerds.net +Tags: SQL, Product Key + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaSqlProductKey + +.EXAMPLE +Get-DbaSqlProductKey winxp, sqlservera, sqlserver2014a, win2k8 +Gets SQL Server versions, editions and product keys for all instances within each server or workstation. + +.EXAMPLE +Get-DbaSqlProductKey -SqlCms sqlserver01 +Gets SQL Server versions, editions and product keys for all instances within sqlserver01's Central Management Server + +.EXAMPLE +Get-DbaSqlProductKey -ServersFromFile C:\Scripts\servers.txt +Gets SQL Server versions, editions and product keys for all instances listed within C:\Scripts\servers.txt +#> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [string[]]$SqlInstances, + # Central Management Server + + [string]$SqlCms, + # File with one server per line + + [string]$ServersFromFile, + [PSCredential]$SqlCredential + ) + + BEGIN { + + Function Unlock-SqlInstanceKey { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [byte[]]$data, + [int]$version + ) + try { + if ($version -ge 11) { $binArray = ($data)[0..66] } + else { $binArray = ($data)[52..66] } + $charsArray = "B", "C", "D", "F", "G", "H", "J", "K", "M", "P", "Q", "R", "T", "V", "W", "X", "Y", "2", "3", "4", "6", "7", "8", "9" + for ($i = 24; $i -ge 0; $i--) { + $k = 0 + for ($j = 14; $j -ge 0; $j--) { + $k = $k * 256 -bxor $binArray[$j] + $binArray[$j] = [math]::truncate($k / 24) + $k = $k % 24 + } + $productKey = $charsArray[$k] + $productKey + if (($i % 5 -eq 0) -and ($i -ne 0)) { + $productKey = "-" + $productKey + } + } + } + catch { $productkey = "Cannot decode product key." } + return $productKey + } + } + + PROCESS { + + if ($SqlCms) { + if ($null -eq [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.RegisteredServers")) + { throw "Can't load CMS assemblies. You must have SQL Server Management Studio installed to use the -SqlCms switch." } + + Write-Verbose "Gathering SQL Servers names from Central Management Server" + $server = Connect-SqlInstance -SqlInstance $SqlCms -SqlCredential $SqlCredential + $sqlconnection = $server.ConnectionContext.SqlConnectionObject + + try { $cmstore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sqlconnection) } + catch { throw "Cannot access Central Management Server" } + $dbstore = $cmstore.DatabaseEngineServerGroup + $SqlInstances = $dbstore.GetDescendantRegisteredServers().servername + # Add the CM server itself, which can't be stored in the CM server. + $servers += $SqlCms + $basenames = @() + foreach ($server in $SqlInstances) { $basenames += $server.Split("\")[0] } + $SqlInstances = $basenames | Get-Unique + } + + If ($ServersFromFile) { + if ((Test-Path $ServersFromFile) -eq $false) { throw "Could not find file: $ServersFromFile" } + $SqlInstances = Get-Content $ServersFromFile + } + + if ([string]::IsNullOrEmpty($SqlInstances)) { $SqlInstances = $env:computername } + + $basepath = "SOFTWARE\Microsoft\Microsoft SQL Server" + # Loop through each server + $objectCollection = @() + foreach ($servername in $SqlInstances) { + $servername = $servername.Split("\")[0] + + if ($servername -eq "." -or $servername -eq "localhost" -or $servername -eq $env:computername) { + $localmachine = [Microsoft.Win32.RegistryHive]::LocalMachine + $defaultview = [Microsoft.Win32.RegistryView]::Default + $reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey($localmachine, $defaultview) + } + else { + # Get IP for remote registry access. It's the most reliable. + try { $ipaddr = ([System.Net.Dns]::GetHostAddresses($servername)).IPAddressToString } + catch { Write-Warning "Can't resolve $servername. Skipping."; continue } + + try { + $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $ipaddr) + } + catch { Write-Warning "Can't access registry for $servername. Is the Remote Registry service started?"; continue } + } + + $instances = $reg.OpenSubKey("$basepath\Instance Names\SQL", $false) + if ($instances -eq $null) { Write-Warning "No instances found on $servername. Skipping."; continue } + # Get Product Keys for all instances on the server. + foreach ($instance in $instances.GetValueNames()) { + if ($instance -eq "MSSQLSERVER") { $SqlInstance = $servername } + else { $SqlInstance = "$servername\$instance" } + + $subkeys = $reg.OpenSubKey("$basepath", $false) + $instancekey = $subkeys.GetSubKeynames() | Where-Object { $_ -like "*.$instance" } + if ($null -eq $instancekey) { $instancekey = $instance } # SQL 2k5 + + # Cluster instance hostnames are required for SMO connection + $cluster = $reg.OpenSubKey("$basepath\$instancekey\Cluster", $false) + if ($cluster -ne $null) { + $clustername = $cluster.GetValue("ClusterName") + if ($instance -eq "MSSQLSERVER") { $SqlInstance = $clustername } + else { $SqlInstance = "$clustername\$instance" } + } + + Write-Verbose "Attempting to connect to $SqlInstance" + try { $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential } + catch { Write-Warning "Can't connect to $SqlInstance or access denied. Skipping."; continue } + + $servicePack = $server.ProductLevel + Write-Debug "$servername $instance version is $($server.VersionMajor)" + switch ($server.VersionMajor) { + 9 { + $sqlversion = "SQL Server 2005 $servicePack" + $findkeys = $reg.OpenSubKey("$basepath\90\ProductID", $false) + foreach ($findkey in $findkeys.GetValueNames()) { + if ($findkey -like "DigitalProductID*") { $key = "$basepath\90\ProductID\$findkey" } + } + } + 10 { + $sqlversion = "SQL Server 2008 $servicePack" + $key = "$basepath\MSSQL10" + if ($server.VersionMinor -eq 50) { $key += "_50"; $sqlversion = "SQL Server 2008 R2 $servicePack" } + $key += ".$instance\Setup\DigitalProductID" + } + 11 { $key = "$basepath\110\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2012 $servicePack" } + 12 { $key = "$basepath\120\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2014 $servicePack" } + 13 { $key = "$basepath\130\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2016 $servicePack" } + default { Write-Warning "SQL version not currently supported."; continue } + } + if ($server.Edition -notlike "*Express*") { + try { + $subkey = Split-Path $key; $binaryvalue = Split-Path $key -leaf + $binarykey = $($reg.OpenSubKey($subkey)).GetValue($binaryvalue) + } + catch { $sqlkey = "Could not connect." } + try { $sqlkey = Unlock-SqlInstanceKey $binarykey $server.VersionMajor } + catch { } + } + else { $sqlkey = "SQL Server Express Edition" } + $server.ConnectionContext.Disconnect() + + $object = New-Object PSObject -Property @{ + "SQL Instance" = $SqlInstance + "SQL Version" = $sqlversion + "SQL Edition" = $server.Edition + "Product Key" = $sqlkey + } + $objectCollection += $object + } + $reg.Close() + } + $objectCollection | Select "SQL Instance", "SQL Version", "SQL Edition", "Product Key" + } + + END { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlServerKey + } +} +function Get-DbaSqlRegistryRoot { + <# +.SYNOPSIS +Uses SQL WMI to find the Registry Root of each SQL Server instance on a computer + +.DESCRIPTION +Uses SQL WMI to find the Registry Root of each SQL Server instance on a computer + +.PARAMETER ComputerName +The target computer. This is not a SQL Server service, though if you pass a named SQL instance, it'll parse properly down to the computer name + +.PARAMETER Credential +Allows you to login to $ComputerName using alternative Windows credentials + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Configuration, Registry + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaSqlRegistryRoot +Gets the registry root for all instances on localhost + +.EXAMPLE +Get-DbaSqlRegistryRoot -ComputerName server1 + +Gets the registry root for all instances on server1 + +#> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($computer in $computername) { + Write-Message -Level Verbose -Message "Connecting to SQL WMI on $($computer.ComputerName)" + try { + $sqlwmis = Invoke-ManagedComputerCommand -ComputerName $computer.ComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -match "SQL Server \(" + } + catch { + Stop-Function -Message $_ -Target $sqlwmi -Continue + } + + foreach ($sqlwmi in $sqlwmis) { + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Write-Message -Level Warning -Message "Can't find instance $vsname on $env:COMPUTERNAME" + return + } + } + + # vsname takes care of clusters + if ([System.String]::IsNullOrEmpty($vsname)) { + $vsname = $computer + if ($instancename -ne "MSSQLSERVER") { + $vsname = "$computer\$instancename" + } + } + + Write-Message -Level Verbose -Message "Regroot: $regroot" + Write-Message -Level Verbose -Message "InstanceName: $instancename" + Write-Message -Level Verbose -Message "VSNAME: $vsname" + + [pscustomobject]@{ + ComputerName = $computer.ComputerName + InstanceName = $instancename + SqlInstance = $vsname + Hive = "HKLM" + Path = $regroot + RegistryRoot = "HKLM:\$regroot" + } + } + } + } +} +function Get-DbaSqlService { + <# + .SYNOPSIS + Gets the SQL Server related services on a computer. + + .DESCRIPTION + Gets the SQL Server related services on one or more computers. + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER InstanceName + Only returns services that belong to the specific instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER Type + Use -Type to collect only services of the desired SqlServiceType. + Can be one of the following: "Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS" + + .PARAMETER ServiceName + Can be used to specify service names explicitly, without looking for service types/instances. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Service, SqlServer, Instance, Connect + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Requires Local Admin rights on destination computer(s). + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlService + + .EXAMPLE + Get-DbaSqlService -ComputerName sqlserver2014a + + Gets the SQL Server related services on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaSqlService + + Gets the SQL Server related services on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaSqlService -ComputerName sql1,sql2 | Out-GridView + + Gets the SQL Server related services on computers sql1 and sql2, and shows them in a grid view. + + .EXAMPLE + Get-DbaSqlService -ComputerName $MyServers -Type SSRS + + Gets the SQL Server related services of type "SSRS" (Reporting Services) on computers in the variable MyServers. + + .EXAMPLE + $services = Get-DbaSqlService -ComputerName sql1 -Type Agent,Engine + $services.ChangeStartMode('Manual') + + Gets the SQL Server related services of types Sql Agent and DB Engine on computer sql1 and changes their startup mode to 'Manual'. + + .EXAMPLE + (Get-DbaSqlService sql1 -Type Engine).Restart($true) + + Calls a Restart method for each Engine service on computer sql1 with -Force option. + #> + [CmdletBinding(DefaultParameterSetName = "Search")] + Param ( + [parameter(ValueFromPipeline = $true, Position = 1)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [Parameter(ParameterSetName = "Search")] + [Alias("Instance")] + [string[]]$InstanceName, + [PSCredential]$Credential, + [Parameter(ParameterSetName = "Search")] + [ValidateSet("Agent", "Browser", "Engine", "FullText", "SSAS", "SSIS", "SSRS")] + [string[]]$Type, + [Parameter(ParameterSetName = "ServiceName")] + [string[]]$ServiceName, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + #Dictionary to transform service type IDs into the names from Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer.Services.Type + $ServiceIdMap = @( + @{ Name = "Engine"; Id = 1 }, + @{ Name = "Agent"; Id = 2 }, + @{ Name = "FullText"; Id = 3, 9 }, + @{ Name = "SSIS"; Id = 4 }, + @{ Name = "SSAS"; Id = 5 }, + @{ Name = "SSRS"; Id = 6 }, + @{ Name = "Browser"; Id = 7 }, + @{ Name = "Unknown"; Id = 8 } + ) + if ($PsCmdlet.ParameterSetName -match 'Search') { + if ($Type) { + $searchClause = "" + foreach ($itemType in $Type) { + foreach ($id in ($ServiceIdMap | Where-Object { $_.Name -eq $itemType }).Id) { + if ($searchClause) { $searchClause += ' OR ' } + $searchClause += "SQLServiceType = $id" + } + } + } + else { + $searchClause = "SQLServiceType > 0" + } + } + elseif ($PsCmdlet.ParameterSetName -match 'ServiceName') { + if ($ServiceName) { + $searchClause = "" + foreach ($sn in $ServiceName) { + if ($searchClause) { $searchClause += ' OR ' } + $searchClause += "ServiceName = '$sn'" + } + } + else { + $searchClause = "SQLServiceType > 0" + } + } + } + PROCESS { + foreach ($Computer in $ComputerName.ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ($Server.FullComputerName) { + $Computer = $server.FullComputerName + Write-Message -Level VeryVerbose -Message "Getting SQL Server namespace on $Computer" -Target $Computer + try { $namespaces = Get-DbaCmObject -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -Query "Select Name FROM __NAMESPACE WHERE Name Like 'ComputerManagement%'" -EnableException -Credential $credential | Sort-Object Name -Descending } + catch { } + if ($namespaces) { + $servicesTemp = @() + + ForEach ($namespace in $namespaces) { + try { + Write-Message -Level Verbose -Message "Getting Cim class SqlService in Namespace $($namespace.Name) on $Computer." -Target $Computer + foreach ($service in (Get-DbaCmObject -ComputerName $Computer -Namespace "root\Microsoft\SQLServer\$($namespace.Name)" -Query "SELECT * FROM SqlService WHERE $searchClause" -EnableException -Credential $credential)) { + $servicesTemp += New-Object PSObject -Property @{ + Name = $service.ServiceName + Namespace = $namespace.Name + Service = $service + } + } + } + catch { + Write-Message -Level Verbose -EnableException $EnableException -Message "Failed to acquire services from namespace $($namespace.Name)." -Target $Computer + } + } + + $services = ($servicesTemp | Group-Object Name | ForEach-Object { $_.Group | Sort-Object Namespace -Descending | Select-Object -First 1 }).Service + + if ($services) { + Write-Message -Level Verbose -EnableException $EnableException -Message "Creating output objects" + ForEach ($service in $services) { + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name ComputerName -Value $service.HostName + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name ServiceType -Value ($ServiceIdMap | Where-Object { $_.Id -contains $service.SQLServiceType }).Name + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name State -Value $(switch ($service.State) { 1 { 'Stopped' } 2 { 'Start Pending' } 3 { 'Stop Pending' } 4 { 'Running' } }) + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name StartMode -Value $(switch ($service.StartMode) { 1 { 'Unknown' } 2 { 'Automatic' } 3 { 'Manual' } 4 { 'Disabled' } }) + + if ($service.ServiceName -in ("MSSQLSERVER", "SQLSERVERAGENT", "ReportServer", "MSSQLServerOLAPService")) { + $instance = "MSSQLSERVER" + } + else { + if ($service.ServiceType -in @("Agent", "Engine", "SSRS", "SSAS")) { + if ($service.ServiceName.indexof('$') -ge 0) { + $instance = $service.ServiceName.split('$')[1] + } + else { + $instance = "Unknown" + } + } + else { + $instance = "" + } + } + $priority = switch ($service.ServiceType) { + "Engine" { 200 } + default { 100 } + } + #If only specific instances are selected + if (!$InstanceName -or $instance -in $InstanceName) { + #Add other properties and methods + Add-Member -Force -InputObject $service -NotePropertyName InstanceName -NotePropertyValue $instance + Add-Member -Force -InputObject $service -NotePropertyName ServicePriority -NotePropertyValue $priority + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Stop" -Value { + Param ([bool]$Force = $false) + Stop-DbaSqlService -InputObject $this -Force:$Force + } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Start" -Value { Start-DbaSqlService -InputObject $this } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Restart" -Value { + Param ([bool]$Force = $false) + Restart-DbaSqlService -InputObject $this -Force:$Force + } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "ChangeStartMode" -Value { + Param ( + [parameter(Mandatory = $true)] + [string]$Mode + ) + $supportedModes = @("Automatic", "Manual", "Disabled") + if ($Mode -notin $supportedModes) { + Stop-Function -Message ("Incorrect mode '$Mode'. Use one of the following values: {0}" -f ($supportedModes -join ' | ')) -EnableException $false -FunctionName 'Get-DbaSqlService' + Return + } + Set-ServiceStartMode -InputObject $this -Mode $Mode -ErrorAction Stop + $this.StartMode = $Mode + } + Select-DefaultView -InputObject $service -Property ComputerName, ServiceName, ServiceType, InstanceName, DisplayName, StartName, State, StartMode -TypeName DbaSqlService + } + } + } + else { + Stop-Function -EnableException $EnableException -Message "No Sql Services found on $Computer" -Continue + } + } + else { + Stop-Function -EnableException $EnableException -Message "No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." -Continue + } + } + else { + Stop-Function -EnableException $EnableException -Message "Failed to connect to $Computer" -Continue + } + } + } +} +function Get-DbaSsisEnvironmentVariable { + <# + .SYNOPSIS + This command gets specified SSIS Environment and all its variables + + .DESCRIPTION + This command gets all variables from specified environment from SSIS Catalog. All sensitive values are decrypted. + The function communicates directly with SSISDB database, "SQL Server Integration Services" service isn't queried there. + Each parameter (besides SqlInstance and SqlCredential) acts as the filter to only include or exclude particular element + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Environment + The SSIS Environments names that we want to get variables from + + .PARAMETER EnvironmentExclude + The SSIS Environments to exclude. Acts as a filter for environments, best used without 'Environment' parameter + to get variables for all environments but excluded ones + + .PARAMETER Folder + The Folders names that contain the environments + + .PARAMETER FolderExclude + The Folders names to exclude. Acts as a filter for folders containing environments, best user without 'Folder' parameter + to get variables for all folders but excluded ones + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SSIS, SSISDB, Variable + Author: Bartosz Ratajczyk ( @b_ratajczyk ) + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSsisEnvironmentVariable + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -Folder DWH_ETL + + Gets variables of 'DEV' environment located in 'DWH_ETL' folder on 'localhost' Server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -Folder DWH_ETL, DEV2, QA + + Gets variables of 'DEV' environment(s) located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -FolderExclude DWH_ETL, DEV2, QA + + Gets variables of 'DEV' environments located in folders other than 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV, PROD -Folder DWH_ETL, DEV2, QA + + Gets variables of 'DEV' and 'PROD' environment(s) located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -EnvironmentExclude DEV, PROD -Folder DWH_ETL, DEV2, QA + + Gets variables of environments other than 'DEV' and 'PROD' located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -EnvironmentExclude DEV, PROD -FolderExclude DWH_ETL, DEV2, QA + + Gets variables of environments other than 'DEV' and 'PROD' located in folders other than 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + 'localhost' | Get-DbaSsisEnvironmentVariable -EnvironmentExclude DEV, PROD + + Gets all SSIS environments except 'DEV' and 'PROD' from 'localhost' server. The server name comes from pipeline + + .EXAMPLE + 'SRV1', 'SRV3' | Get-DbaSsisEnvironmentVariable + + Gets all SSIS environments from 'SRV1' and 'SRV3' servers. The server's names come from pipeline + + .EXAMPLE + 'SRV1', 'SRV2' | Get-DbaSsisEnvironmentVariable DEV | Out-GridView + + Gets all variables from 'DEV' Environment(s) on servers 'SRV1' and 'SRV2' and outputs it as the GridView. + The server names come from the pipeline. + + .EXAMPLE + 'localhost' | Get-DbaSsisEnvironmentVariable -EnvironmentExclude DEV, PROD | Select-Object -Property Name, Value | Where-Object {$_.Name -match '^a'} | Out-GridView + + Gets all variables from Environments other than 'DEV' and 'PROD' on 'localhost' server, + selects Name and Value properties for variables that names start with letter 'a' and outputs it as the GridView + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('SqlServer', 'ServerInstance')] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [parameter(Mandatory = $false)] + [object[]]$Environment, + [parameter(Mandatory = $false)] + [object[]]$EnvironmentExclude, + [parameter(Mandatory = $false)] + [object[]]$Folder, + [parameter(Mandatory = $false)] + [object[]]$FolderExclude, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Message "Connecting to $instance" -Level Verbose + $server = Connect-SqlInstance -SqlInstance $instance -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices" + + Write-Message -Message "Connecting to SSIS Catalog on $instance" -Level Verbose + $SSIS = New-Object "$ISNamespace.IntegrationServices" $server + } + catch { + Stop-Function -Message "Could not connect to SSIS Catalog on $instance or current SMO library does not support SSIS catalog" + return + } + + Write-Message -Message "Fetching SSIS Catalog and its folders" -Level Verbose + $catalog = $SSIS.Catalogs | Where-Object { $_.Name -eq "SSISDB" } + + # get all folders names if none provided + if ($null -eq $Folder) { + $searchFolders = $catalog.Folders.Name + } + else { + $searchFolders = $Folder + } + + # filter unwanted folders + if ($FolderExclude) { + $searchFolders = $searchFolders | Where-Object { $_ -notin $FolderExclude } + } + + if ($null -eq $searchFolders) { + Write-Message -Message "Instance: $instance > -Folder and -FolderExclude filters return an empty collection. Skipping" -Level Warning + } + else { + foreach ($f in $searchFolders) { + # get all environments names if none provided + if ($null -eq $Environment) { + $searchEnvironments = $catalog.Folders.Environments.Name + } + else { + $searchEnvironments = $Environment + } + + #filter unwanted environments + if ($EnvironmentExclude) { + $searchEnvironments = $searchEnvironments | Where-Object { $_ -notin $EnvironmentExclude } + } + + if ($null -eq $searchEnvironments) { + Write-Message -Message "Instance: $instance / Folder: $f > -Environment and -EnvironmentExclude filters return an empty collection. Skipping." -Level Warning + } + else { + $Environments = $catalog.Folders[$f].Environments | Where-Object { $_.Name -in $searchEnvironments } + + foreach ($e in $Environments) { + #encryption handling + $encKey = 'MS_Enckey_Env_' + $e.EnvironmentId + $encCert = 'MS_Cert_Env_' + $e.EnvironmentId + + <# + SMO does not return sensitive values (gets data from catalog.environment_variables) + We have to manually query internal.environment_variables instead and use symmetric keys + within T-SQL code + #> + + $sql = @" + OPEN SYMMETRIC KEY $encKey DECRYPTION BY CERTIFICATE $encCert; + + SELECT + ev.variable_id, + ev.name, + ev.description, + ev.type, + ev.sensitive, + value = ev.value, + ev.sensitive_value, + ev.base_data_type, + decrypted = decrypted.value + FROM internal.environment_variables ev + + CROSS APPLY ( + SELECT + value = CASE base_data_type + WHEN 'nvarchar' THEN CONVERT(NVARCHAR(MAX), DECRYPTBYKEY(sensitive_value)) + WHEN 'bit' THEN CONVERT(NVARCHAR(MAX), CONVERT(bit, DECRYPTBYKEY(sensitive_value))) + WHEN 'datetime' THEN CONVERT(NVARCHAR(MAX), CONVERT(datetime2(0), DECRYPTBYKEY(sensitive_value))) + WHEN 'single' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'float' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'decimal' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'tinyint' THEN CONVERT(NVARCHAR(MAX), CONVERT(tinyint, DECRYPTBYKEY(sensitive_value))) + WHEN 'smallint' THEN CONVERT(NVARCHAR(MAX), CONVERT(smallint, DECRYPTBYKEY(sensitive_value))) + WHEN 'int' THEN CONVERT(NVARCHAR(MAX), CONVERT(INT, DECRYPTBYKEY(sensitive_value))) + WHEN 'bigint' THEN CONVERT(NVARCHAR(MAX), CONVERT(bigint, DECRYPTBYKEY(sensitive_value))) + END + ) decrypted + WHERE environment_id = $($e.EnvironmentId); + CLOSE SYMMETRIC KEY $encKey; +"@ + + $ssisVariables = $server.Query($sql, "SSISDB") + + foreach ($variable in $ssisVariables) { + if ($variable.sensitive -eq $true) { + $value = $variable.decrypted + } + else { + $value = $variable.value + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Folder = $f + Environment = $e.Name + Id = $variable.variable_id + Name = $variable.Name + Description = $variable.description + Type = $variable.type + IsSensitive = $variable.sensitive + BaseDataType = $variable.base_data_type + Value = $value + } + } + } + } + } + } + } + } +} +function Get-DbaStartupParameter { + <# + .SYNOPSIS + Displays values for a detailed list of SQL Server Startup Parameters. + + .DESCRIPTION + Displays values for a detailed list of SQL Server Startup Parameters including Master Data Path, Master Log path, Error Log, Trace Flags, Parameter String and much more. + + This command relies on remote Windows Server (SQL WMI/WinRm) access. You can pass alternative Windows credentials by using the -Credential parameter. + + See https://msdn.microsoft.com/en-us/library/ms190737.aspx for more information. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER Credential + Allows you to login to servers using alternate Windows credentials. + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Simple + If this switch is enabled, simplified output will be produced including only Server, Master Data Path, Master Log path, ErrorLog, TraceFlags and ParameterString. + + .PARAMETER EnableException + If this switch is enabled, exceptions will be thrown to the caller, which will need to perform its own exception processing. Otherwise, the function will try to catch the exception, interpret it and provide a friendly error message. + + .EXAMPLE + Get-DbaStartupParameter -SqlInstance sql2014 + + Logs into SQL WMI as the current user then displays the values for numerous startup parameters. + + .EXAMPLE + $wincred = Get-Credential ad\sqladmin + Get-DbaStartupParameter -SqlInstance sql2014 -Credential $wincred -Simple + + Logs in to WMI using the ad\sqladmin credential and gathers simplified information about the SQL Server Startup Parameters. + + .NOTES + Tags: WSMan, SQLWMI, Memory + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaStartupParameter +#> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("SqlCredential")] + [PSCredential]$Credential, + [switch]$Simple, + [switch] + [Alias('Silent')]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $computerName = $instance.ComputerName + $instanceName = $instance.InstanceName + $ogInstance = $instance.FullSmoName + + $computerName = (Resolve-DbaNetworkName -ComputerName $computerName).FullComputerName + + Write-Message -Level Verbose -message "Attempting to connect to $computerName" + + if ($instanceName.Length -eq 0) { $instanceName = "MSSQLSERVER" } + + $displayname = "SQL Server ($instanceName)" + + $Scriptblock = { + $computerName = $args[0] + $displayname = $args[1] + + $wmisvc = $wmi.Services | Where-Object DisplayName -eq $displayname + + $params = $wmisvc.StartupParameters -split ';' + + $masterdata = $params | Where-Object { $_.StartsWith('-d') } + $masterlog = $params | Where-Object { $_.StartsWith('-l') } + $errorlog = $params | Where-Object { $_.StartsWith('-e') } + $traceflags = $params | Where-Object { $_.StartsWith('-T') } + + $debugflag = $params | Where-Object { $_.StartsWith('-t') } + + if ($debugflag.length -ne 0) { + Write-Message -Level Warning "$instance is using the lowercase -t trace flag. This is for internal debugging only. Please ensure this was intentional." + } + #> + + if ($traceflags.length -eq 0) { + $traceflags = "None" + } + else { + $traceflags = $traceflags.substring(2) + } + + if ($Simple -eq $true) { + [PSCustomObject]@{ + ComputerName = $computerName + InstanceName = $instanceName + SqlInstance = $ogInstance + MasterData = $masterdata.TrimStart('-d') + MasterLog = $masterlog.TrimStart('-l') + ErrorLog = $errorlog.TrimStart('-e') + TraceFlags = $traceflags -join ',' + ParameterString = $wmisvc.StartupParameters + } + } + else { + # From https://msdn.microsoft.com/en-us/library/ms190737.aspx + + $commandpromptparm = $params | Where-Object { $_ -eq '-c' } + $minimalstartparm = $params | Where-Object { $_ -eq '-f' } + $memorytoreserve = $params | Where-Object { $_.StartsWith('-g') } + $noeventlogsparm = $params | Where-Object { $_ -eq '-n' } + $instancestartparm = $params | Where-Object { $_ -eq '-s' } + $disablemonitoringparm = $params | Where-Object { $_ -eq '-x' } + $increasedextentsparm = $params | Where-Object { $_ -ceq '-E' } + + $minimalstart = $noeventlogs = $instancestart = $disablemonitoring = $false + $increasedextents = $commandprompt = $singleuser = $false + + if ($null -ne $commandpromptparm) { + $commandprompt = $true + } + if ($null -ne $minimalstartparm) { + $minimalstart = $true + } + if ($null -eq $memorytoreserve) { + $memorytoreserve = 0 + } + if ($null -ne $noeventlogsparm) { + $noeventlogs = $true + } + if ($null -ne $instancestartparm) { + $instancestart = $true + } + if ($null -ne $disablemonitoringparm) { + $disablemonitoring = $true + } + if ($null -ne $increasedextentsparm) { + $increasedextents = $true + } + + $singleuserparm = $params | Where-Object { $_.StartsWith('-m') } + + if ($singleuserparm.length -ne 0) { + $singleuser = $true + $singleuserdetails = $singleuserparm.TrimStart('-m') + } + + [PSCustomObject]@{ + ComputerName = $computerName + InstanceName = $instanceName + SqlInstance = $ogInstance + MasterData = $masterdata -replace '^-[dD]', '' + MasterLog = $masterlog -replace '^-[lL]', '' + ErrorLog = $errorlog -replace '^-[eE]', '' + TraceFlags = $traceflags -join ',' + CommandPromptStart = $commandprompt + MinimalStart = $minimalstart + MemoryToReserve = $memorytoreserve + SingleUser = $singleuser + SingleUserName = $singleuserdetails + NoLoggingToWinEvents = $noeventlogs + StartAsNamedInstance = $instancestart + DisableMonitoring = $disablemonitoring + IncreasedExtents = $increasedextents + ParameterString = $wmisvc.StartupParameters + } + } + } + + # This command is in the internal function + # It's sorta like Invoke-Command. + if ($credential) { + Invoke-ManagedComputerCommand -Server $computerName -Credential $credential -ScriptBlock $Scriptblock -ArgumentList $computerName, $displayname + } + else { + Invoke-ManagedComputerCommand -Server $computerName -ScriptBlock $Scriptblock -ArgumentList $computerName, $displayname + } + } + catch { + Stop-Function -Message "$instance failed." -ErrorRecord $_ -Continue -Target $instance + } + } + } +} +function Get-DbaSuspectPage { + <# + .SYNOPSIS + Returns data that is stored in SQL for Suspect Pages on the specified SQL Server Instance + + .DESCRIPTION + This function returns any records that were stored due to suspect pages in databases on a SQL Server Instance. + + .PARAMETER SqlInstance + A SQL Server instance to connect to + + .PARAMETER SqlCredential + A credential to use to connect to the SQL Instance rather than using Windows Authentication + + .PARAMETER Database + The database to return. If unspecified, all records will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Pages, DBCC + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaSuspectPage -SqlInstance sql2016 + + Retrieve any records stored for Suspect Pages on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaSuspectPage -SqlInstance sql2016 -Database Test + + Retrieve any records stored for Suspect Pages on the sql2016 SQL Server and the Test database only. + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [object]$Database, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlinstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + $sql = "Select + DB_NAME(database_id) as DBName, + file_id, + page_id, + CASE event_type + WHEN 1 THEN '823 or 824 or Torn Page' + WHEN 2 THEN 'Bad Checksum' + WHEN 3 THEN 'Torn Page' + WHEN 4 THEN 'Restored' + WHEN 5 THEN 'Repaired (DBCC)' + WHEN 7 THEN 'Deallocated (DBCC)' + END as EventType, + error_count, + last_update_date + from msdb.dbo.suspect_pages" + + try { + $results = $server.Query($sql) + } + catch { + Stop-Function -Message "Issue collecting data on $server" -Target $server -ErrorRecord $_ -Continue + } + + if ($Database) { + $results = $results | Where-Object DBName -EQ $Database + } + + } + foreach ($row in $results) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DBName + FileId = $row.file_id + PageId = $row.page_id + EventType = $row.EventType + ErrorCount = $row.error_count + LastUpdateDate = $row.last_update_date + } + } + } +} +function Get-DbaTable { + <# +.SYNOPSIS +Returns a summary of information on the tables + +.DESCRIPTION +Shows table information around table row and data sizes and if it has any table type information. + +.PARAMETER SqlInstance +SQL Server name or SMO object representing the SQL Server to connect to. This can be a +collection and receive pipeline input + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER IncludeSystemDBs +Switch parameter that when used will display system database information + +.PARAMETER Table +Define a specific table you would like to query. You can specify up to three-part name like db.sch.tbl. +If the object has special characters please wrap them in square brackets [ ]. +This dbo.First.Table will try to find table named 'Table' on schema 'First' and database 'dbo'. +The correct way to find table named 'First.Table' on schema 'dbo' is passing dbo.[First.Table] + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database, Tables +Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaTable + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Database Test1 +Return all tables in the Test1 database + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Database MyDB -Table MyTable +Return only information on the table MyTable from the database MyDB + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Table MyTable +Returns information on table called MyTable if it exists in any database on the server, under any schema + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Table dbo.[First.Table] +Returns information on table called First.Table on schema dbo if it exists in any database on the server + +.EXAMPLE +'localhost','localhost\namedinstance' | Get-DbaTable -Database DBA -Table Commandlog +Returns information on the CommandLog table in the DBA database on both instances localhost and the named instance localhost\namedinstance + +#> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystemDBs, + [string[]]$Table, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($Table) { + $fqtns = @() + foreach ($t in $Table) { + $splitName = [regex]::Matches($t, "(\[.+?\])|([^\.]+)").Value + $dotcount = $splitName.Count + + $splitDb = $Schema = $null + + switch ($dotcount) { + 1 { + $tbl = $t + } + 2 { + $schema = $splitName[0] + $tbl = $splitName[1] + } + 3 { + $splitDb = $splitName[0] + $schema = $splitName[1] + $tbl = $splitName[2] + } + default { + Write-Message -Level Warning -Message "Please make sure that you are using up to three-part names. If your search value contains '.' character you must use [ ] to wrap the name. The value $t is not a valid name." + Continue + } + } + + if ($splitDb -like "[[]*[]]") { + $splitDb = $splitDb.Substring(1, ($splitDb.Length - 2)) + } + + if ($schema -like "[[]*[]]") { + $schema = $schema.Substring(1, ($schema.Length - 2)) + } + + if ($tbl -like "[[]*[]]") { + $tbl = $tbl.Substring(1, ($tbl.Length - 2)) + } + + $fqtns += [PSCustomObject] @{ + Database = $splitDb + Schema = $Schema + Table = $tbl + } + } + } + } + + process { + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + #only look at online databases (Status equal normal) + $dbs = $server.Databases | Where-Object IsAccessible + + #If IncludeSystemDBs is false, exclude systemdbs + if (!$IncludeSystemDBs -and !$Database) { + $dbs = $dbs | Where-Object { !$_.IsSystemObject } + } + + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + catch { + Stop-Function -Message "Unable to gather dbs for $instance" -Target $instance -Continue -ErrorRecord $_ + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db" + + if ($fqtns) { + $tables = @() + foreach ($fqtn in $fqtns) { + # If the user specified a database in a three-part name, and it's not the + # database currently being processed, skip this table. + if ($fqtn.Database) { + if ($fqtn.Database -ne $db.Name) { + continue + } + } + + $tbl = $db.tables | Where-Object { $_.Name -in $fqtn.Table -and $fqtn.Schema -in ($_.Schema, $null) -and $fqtn.Database -in ($_.Parent.Name, $null) } + + if (-not $tbl) { + Write-Message -Level Verbose -Message "Could not find table $($fqtn.Table) in $db on $server" + } + $tables += $tbl + } + } + else { + $tables = $db.Tables + } + + foreach ($sqltable in $tables) { + $sqltable | Add-Member -Force -MemberType NoteProperty -Name ComputerName -Value $server.NetName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name Database -Value $db.Name + + $defaultprops = "ComputerName", "InstanceName", "SqlInstance", "Database", "Schema", "Name", "IndexSpaceUsed", "DataSpaceUsed", "RowCount", "HasClusteredIndex", "IsFileTable", "IsMemoryOptimized", "IsPartitioned", "FullTextIndex", "ChangeTrackingEnabled" + + Select-DefaultView -InputObject $sqltable -Property $defaultprops + } + } + } + } +} +function Get-DbaTcpPort { + <# + .SYNOPSIS + Returns the TCP port used by the specified SQL Server. + + .DESCRIPTION + By default, this function returns just the TCP port used by the specified SQL Server. + + If -Detailed is specified, the server name, IPAddress (ipv4 and ipv6), port number and an indicator of whether or not the port assignment is static are returned. + + Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Allows you to connect to servers using alternate Windows credentials + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + .PARAMETER Detailed + If this switch is enabled, an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for one or more SQL Servers is returned. + + .PARAMETER ExcludeIpv6 + If this switch is enabled, IPv6 information is excluded from detailed output. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SQLWMI, tcp + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTcpPort + + .EXAMPLE + Get-DbaTcpPort -SqlInstance sqlserver2014a + + Returns just the port number for the default instance on sqlserver2014a. + + .EXAMPLE + Get-DbaTcpPort -SqlInstance winserver\sqlexpress, sql2016 + + Returns an object with server name and port number for the sqlexpress on winserver and the default instance on sql2016. + + .EXAMPLE + Get-DbaTcpPort -SqlInstance sqlserver2014a, sql2016 -Detailed + + Returns an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for sqlserver2014a and sql2016. + + Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaTcpPort -ExcludeIpv6 -Detailed + + Returns an object with server name, IPAddress (just ipv4), port and static ($true/$false) for every server listed in the Central Management Server on sql2014. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [switch]$Detailed, + [Alias("Ipv4")] + [switch]$ExcludeIpv6, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + if ($detailed -eq $true) { + try { + $scriptblock = { + $instance = $args[0] + + Add-Type -AssemblyName Microsoft.VisualBasic + + foreach ($servername in $wmi.ServerInstances) { + $instanceName = $servername.Name + $wmiinstance = $wmi.Services | Where-Object { $_.DisplayName -eq "SQL Server ($instanceName)" } + $vsname = ($wmiinstance.AdvancedProperties | Where-Object { $_ -match 'VSNAME' }).Value + + if ($vsname.length -eq 0) { + $vsname = "$instance\$instanceName" + } + + $vsname = $vsname.Replace("\MSSQLSERVER", "") + + try { + $regroot = ($wmiinstance.AdvancedProperties | Where-Object { $_ -match 'REGROOT' }).Value + $dacport = (Get-ItemProperty "HKLM:\$regroot\MSSQLServer\SuperSocketNetLib\AdminConnection\Tcp").TcpDynamicPorts + + [PsCustomObject]@{ + ComputerName = $instance + InstanceName = $instanceName + SqlInstance = $vsname + IPAddress = "0.0.0.0" + Port = $dacport + Static = $false + Type = "DAC" + } + } + catch { + # it's just not our day + } + + $tcp = $servername.ServerProtocols | Where-Object Name -eq Tcp + $ips = $tcp.IPAddresses + + # This is a remote command so do not use Write-message + Write-Verbose "Parsing information for $($ips.count) IP addresses." + foreach ($ip in $ips) { + $props = $ip.IPAddressProperties | Where-Object { $_.Name -eq "TcpPort" -or $_.Name -eq "TcpDynamicPorts" } + + foreach ($prop in $props) { + if ([Microsoft.VisualBasic.Information]::IsNumeric($prop.value)) { + $port = $prop.value + if ($prop.name -eq 'TcpPort') { + $static = $true + } + else { + $static = $false + } + break + } + } + + [PsCustomObject]@{ + ComputerName = $instance + InstanceName = $instanceName + SqlInstance = $vsname + IPAddress = $ip.IPAddress.IPAddressToString + Port = $port + Static = $static + Type = "Normal" + } + } + } + } + + $computer = $instance.ComputerName + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Verbose:$false + $computername = $resolved.FullComputerName + + try { + Write-Message -Level Verbose -Message "Trying with ComputerName ($computer)." + $someIps = Invoke-ManagedComputerCommand -ComputerName $computer -ArgumentList $computer -ScriptBlock $scriptblock + } + catch { + Write-Message -Level Verbose -Message "Trying with FullComputerName because ComputerName failed." + $someIps = Invoke-ManagedComputerCommand -ComputerName $computername -ArgumentList $fqdn -ScriptBlock $scriptblock + } + } + catch { + Stop-Function -Message "Could not get detailed information." -Target $instance -ErrorRecord $_ + } + + $cleanedUp = $someIps | Sort-Object IPAddress + + if ($ExcludeIpv6) { + $octet = '(?:0?0?[0-9]|0?[1-9][0-9]|1[0-9]{2}|2[0-5][0-5]|2[0-4][0-9])' + [regex]$ipv4 = "^(?:$octet\.){3}$octet$" + $cleanedUp = $cleanedUp | Where-Object { $_.IPAddress -match $ipv4 } + } + + $cleanedUp + } + + if ($Detailed -eq $false -or ($Detailed -eq $true -and $null -eq $someIps)) { + try { + $server = Connect-SqlInstance -SqlInstance "TCP:$instance" -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $servername -Continue + } + + # WmiComputer can be unreliable :( Use T-SQL + $sql = "SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID" + $port = $server.Query($sql) + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Port = $port.local_tcp_port + } + } + } + } +} +function Get-DbaTempdbUsage { + <# + .SYNOPSIS + Gets Tempdb usage for running queries. + + .DESCRIPTION + This function queries DMVs for running sessions using Tempdb and returns results if those sessions have user or internal space allocated or deallocated against them. + + .PARAMETER SqlInstance + The SQL Instance you are querying against. + + .PARAMETER SqlCredential + If you want to use alternative credentials to connect to the server. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Tempdb, Space + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTempdbUsage + + .EXAMPLE + Get-DbaTempdbUsage -SqlInstance localhost\SQLDEV2K14 + + Gets tempdb usage for localhost\SQLDEV2K14 + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -le 9) { + Stop-Function -Message "This function is only supported in SQL Server 2008 or higher." -Continue + } + + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + t.session_id AS Spid, + r.command AS StatementCommand, + SUBSTRING( est.[text], + (r.statement_start_offset / 2) + 1, + ((CASE r.statement_end_offset + WHEN-1 + THEN DATALENGTH(est.[text]) + ELSE + r.statement_end_offset + END - r.statement_start_offset + ) / 2 + ) + 1 + ) AS QueryText, + QUOTENAME(DB_NAME(r.database_id)) + N'.' + QUOTENAME(OBJECT_SCHEMA_NAME(est.objectid, est.dbid)) + N'.' + + QUOTENAME(OBJECT_NAME(est.objectid, est.dbid)) AS ProcedureName, + r.start_time AS StartTime, + tdb.UserObjectAllocated * 8 AS CurrentUserAllocatedKB, + (t.user_objects_alloc_page_count + tdb.UserObjectAllocated) * 8 AS TotalUserAllocatedKB, + tdb.UserObjectDeallocated * 8 AS UserDeallocatedKB, + (t.user_objects_dealloc_page_count + tdb.UserObjectDeallocated) * 8 AS TotalUserDeallocatedKB, + tdb.InternalObjectAllocated * 8 AS InternalAllocatedKB, + (t.internal_objects_alloc_page_count + tdb.InternalObjectAllocated) * 8 AS TotalInternalAllocatedKB, + tdb.InternalObjectDeallocated * 8 AS InternalDeallocatedKB, + (t.internal_objects_dealloc_page_count + tdb.InternalObjectDeallocated) * 8 AS TotalInternalDeallocatedKB, + r.reads AS RequestedReads, + r.writes AS RequestedWrites, + r.logical_reads AS RequestedLogicalReads, + r.cpu_time AS RequestedCPUTime, + s.is_user_process AS IsUserProcess, + s.[status] AS [Status], + DB_NAME(r.database_id) AS [Database], + s.login_name AS LoginName, + s.original_login_name AS OriginalLoginName, + s.nt_domain AS NTDomain, + s.nt_user_name AS NTUserName, + s.[host_name] AS HostName, + s.[program_name] AS ProgramName, + s.login_time AS LoginTime, + s.last_request_start_time AS LastRequestedStartTime, + s.last_request_end_time AS LastRequestedEndTime +FROM sys.dm_db_session_space_usage AS t +INNER JOIN sys.dm_exec_sessions AS s + ON s.session_id = t.session_id +LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id +LEFT JOIN + ( SELECT _tsu.session_id, + _tsu.request_id, + SUM(_tsu.user_objects_alloc_page_count) AS UserObjectAllocated, + SUM(_tsu.user_objects_dealloc_page_count) AS UserObjectDeallocated, + SUM(_tsu.internal_objects_alloc_page_count) AS InternalObjectAllocated, + SUM(_tsu.internal_objects_dealloc_page_count) AS InternalObjectDeallocated + FROM tempdb.sys.dm_db_task_space_usage AS _tsu + GROUP BY _tsu.session_id, + _tsu.request_id + ) AS tdb + ON tdb.session_id = r.session_id + AND tdb.request_id = r.request_id +OUTER APPLY sys.dm_exec_sql_text(r.[sql_handle]) AS est +WHERE t.session_id != @@SPID + AND (tdb.UserObjectAllocated - tdb.UserObjectDeallocated + tdb.InternalObjectAllocated - tdb.InternalObjectDeallocated) != 0 +OPTION (RECOMPILE);" + + $server.Query($sql) + } + } +} +function Get-DbatoolsLog { + <# + .SYNOPSIS + Returns log entries for dbatools + + .DESCRIPTION + Returns log entries for dbatools. Handy when debugging or developing for it. Also used when preparing a support package. + + .PARAMETER Errors + Instead of log entries, the error entries will be retrieved + + .NOTES + Author: Fred Weinmann (@FredWeinmann) + Tags: Debug + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbatoolsLog + + .EXAMPLE + Get-DbatoolsLog + + Returns all log entries currently in memory. + #> + [CmdletBinding()] + param + ( + [switch] + $Errors + ) + + BEGIN { + # No Write-Message, since that would clutter the very log you want to retrieve + } + + PROCESS { + if ($Errors) { return [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::GetErrors() } + else { return [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::GetLog() } + } + + END { + + } +} +function Get-DbaTopResourceUsage { + <# + .SYNOPSIS + Returns the top 20 resource consumers for cached queries based on four different metrics: duration, frequency, IO, and CPU. + + .DESCRIPTION + Returns the top 20 resource consumers for cached queries based on four different metrics: duration, frequency, IO, and CPU. + + This command is based off of queries provided by Michael J. Swart at http://michaeljswart.com/go/Top20 + + Per Michael: "I've posted queries like this before, and others have written many other versions of this query. All these queries are based on sys.dm_exec_query_stats." + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER ExcludeSystem + This will exclude system objects like replication procedures from being returned. + + .PARAMETER Type + By default, all Types run but you can specify one or more of the following: Duration, Frequency, IO, or CPU + + .PARAMETER Limit + By default, these query the Top 20 worst offenders (though more than 20 results can be returend if each of the top 20 have more than 1 subsequent result) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Query, Performance + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTopResourceUsage + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008, sql2012 + Return the 80 (20 x 4 types) top usage results by duration, frequency, IO, and CPU servers for servers sql2008 and sql2012 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008 -Type Duration, Frequency -Database TestDB + Return the highest usage by duration (top 20) and frequency (top 20) for the TestDB on sql2008 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2016 -Limit 30 + Return the highest usage by duration (top 30) and frequency (top 30) for the TestDB on sql2016 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008, sql2012 -ExcludeSystem + Return the 80 (20 x 4 types) top usage results by duration, frequency, IO, and CPU servers for servers sql2008 and sql2012 without any System Objects + + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2016| Select * + Return all the columns plus the QueryPlan column + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [ValidateSet("All", "Duration", "Frequency", "IO", "CPU")] + [string[]]$Type = "All", + [int]$Limit = 20, + [Alias('Silent')] + [switch]$EnableException, + [switch]$ExcludeSystem + ) + + begin { + + $instancecolumns = " SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, " + + if ($database) { + $wheredb = " and coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') in ('$($database -join '', '')')" + } + + if ($ExcludeDatabase) { + $wherenotdb = " and coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') notin '$($excludedatabase -join '', '')'" + } + + if ($ExcludeSystem) { + $whereexcludesystem = " AND coalesce(object_name(st.objectid, st.dbid), '') NOT LIKE 'sp_MS%' " + } + $duration = ";with long_queries as + ( + select top $Limit + query_hash, + sum(total_elapsed_time) elapsed_time + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_elapsed_time) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_elapsed_time / 1000 as TotalElapsedTimeMs, + qs.execution_count as ExecutionCount, + cast((total_elapsed_time / 1000) / (execution_count + 0.0) as money) as AverageDurationMs, + lq.elapsed_time / 1000 as QueryTotalElapsedTimeMs, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join long_queries lq + on lq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by lq.elapsed_time desc, + lq.query_hash, + qs.total_elapsed_time desc + option (recompile)" + + $frequency = ";with frequent_queries as + ( + select top $Limit + query_hash, + sum(execution_count) executions + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(execution_count) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.execution_count as ExecutionCount, + executions as QueryTotalExecutions, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join frequent_queries fq + on fq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by fq.executions desc, + fq.query_hash, + qs.execution_count desc + option (recompile)" + + $io = ";with high_io_queries as + ( + select top $Limit + query_hash, + sum(total_logical_reads + total_logical_writes) io + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_logical_reads + total_logical_writes) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_logical_reads + total_logical_writes as TotalIO, + qs.execution_count as ExecutionCount, + cast((total_logical_reads + total_logical_writes) / (execution_count + 0.0) as money) as AverageIO, + io as QueryTotalIO, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join high_io_queries fq + on fq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by fq.io desc, + fq.query_hash, + qs.total_logical_reads + total_logical_writes desc + option (recompile)" + + $cpu = ";with high_cpu_queries as + ( + select top $Limit + query_hash, + sum(total_worker_time) cpuTime + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_worker_time) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_worker_time as CpuTime, + qs.execution_count as ExecutionCount, + cast(total_worker_time / (execution_count + 0.0) as money) as AverageCpuMs, + cpuTime as QueryTotalCpu, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join high_cpu_queries hcq + on hcq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by hcq.cpuTime desc, + hcq.query_hash, + qs.total_worker_time desc + option (recompile)" + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($server.ConnectionContext.StatementTimeout -ne 0) { + $server.ConnectionContext.StatementTimeout = 0 + } + + if ($Type -in "All", "Duration") { + try { + Write-Message -Level Debug -Message "Executing SQL: $duration" + $server.Query($duration) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for duration." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "Frequency") { + try { + Write-Message -Level Debug -Message "Executing SQL: $frequency" + $server.Query($frequency) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for frequency." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "IO") { + try { + Write-Message -Level Debug -Message "Executing SQL: $io" + $server.Query($io) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for IO." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "CPU") { + try { + Write-Message -Level Debug -Message "Executing SQL: $cpu" + $server.Query($cpu) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for CPU." -ErrorRecord $_ -Target $server -Continue + } + } + } + } +} +function Get-DbaTrace { + <# + .SYNOPSIS + Gets a list of trace(s) from specified SQL Server Instance + + .DESCRIPTION + This function returns a list of traces on a SQL Server instance and identifies the default trace file + + .PARAMETER SqlInstance + A SQL Server instance to connect to + + .PARAMETER SqlCredential + A credential to use to connect to the SQL Instance rather than using Windows Authentication + + .PARAMETER Id + The id(s) of the Trace + + .PARAMETER Default + Switch that will only return the information for the default system trace + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2016 + + Lists all the tracefiles on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2016 -Default + + Lists the default trace information on the sql2016 SQL Server. + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int[]]$Id, + [switch]$Default, + [switch][Alias('Silent')] + $EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaTraceFile + + # A Microsoft.SqlServer.Management.Trace.TraceServer class exists but is buggy + # and requires x86 PowerShell. So we'll go with T-SQL. + $sql = "SELECT id, status, path, max_size, stop_time, max_files, is_rowset, is_rollover, is_shutdown, is_default, buffer_count, buffer_size, file_position, reader_spid, start_time, last_event_time, event_count, dropped_event_count FROM sys.traces" + + if ($Id) { + $idstring = $Id -join "," + $sql = "$sql WHERE id in ($idstring)" + } + } + process { + foreach ($instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + try { + $results = $server.Query($sql) + } + catch { + Stop-Function -Message "Issue collecting trace data on $server" -Target $server -ErrorRecord $_ + } + + if ($Default) { + $results = $results | Where-Object { $_.is_default } + } + + foreach ($row in $results) { + if ($row.Path.ToString().Length -gt 0) { + $remotefile = Join-AdminUnc -servername $server.NetName -filepath $row.path + } + else { + $remotefile = $null + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Id = $row.id + Status = $row.status + IsRunning = ($row.status -eq 1) + Path = $row.path + RemotePath = $remotefile + MaxSize = $row.max_size + StopTime = $row.stop_time + MaxFiles = $row.max_files + IsRowset = $row.is_rowset + IsRollover = $row.is_rollover + IsShutdown = $row.is_shutdown + IsDefault = $row.is_default + BufferCount = $row.buffer_count + BufferSize = $row.buffer_size + FilePosition = $row.file_position + ReaderSpid = $row.reader_spid + StartTime = $row.start_time + LastEventTime = $row.last_event_time + EventCount = $row.event_count + DroppedEventCount = $row.dropped_event_count + Parent = $server + SqlCredential = $SqlCredential + } | Select-DefaultView -ExcludeProperty Parent, RemotePath, RemoStatus, SqlCredential + } + } + } +} +function Get-DbaTraceFlag { + <# + .SYNOPSIS + Get global Trace Flag(s) information for each instance(s) of SQL Server. + + .DESCRIPTION + Returns Trace Flags that are enabled globally on each instance(s) of SQL Server as an object. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER TraceFlag + Use this switch to filter to a specific Trace Flag. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: TraceFlag + Author: Kevin Bullen (@sqlpadawan) + + References: https://docs.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-traceon-trace-flags-transact-sql + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTraceFlag + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost + + Returns all Trace Flag information on the local default SQL Server instance + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost, sql2016 + + Returns all Trace Flag(s) for the local and sql2016 SQL Server instances + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost -TraceFlag 4199,3205 + + Returns Trace Flag status for TF 4199 and 3205 for the local SQL Server instance if they are enabled. + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [int[]]$TraceFlag, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $tflags = $server.EnumActiveGlobalTraceFlags() + + if ($tFlags.Rows.Count -eq 0) { + Write-Message -Level Output -Message "No global trace flags enabled" + return + } + + if ($TraceFlag) { + $tflags = $tflags | Where-Object TraceFlag -In $TraceFlag + } + + foreach ($tflag in $tflags) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TraceFlag = $tflag.TraceFlag + Global = $tflag.Global + Session = $tflag.Session + Status = $tflag.Status + } | Select-DefaultView -ExcludeProperty 'Session' + } + } + } +} +function Get-DbaTrigger { + <# +.SYNOPSIS +Get all existing triggers on one or more SQL instances. + +.DESCRIPTION +Get all existing triggers on one or more SQL instances. + +Default output includes columns ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. + +.PARAMETER SqlInstance +The SQL Instance that you're connecting to. + +.PARAMETER SqlCredential +SqlCredential object used to connect to the SQL Server as a different user. + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.NOTES +Tags: Database, Triggers +Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK + https://dbatools.io/Get-DbaTrigger + +.EXAMPLE +Get-DbaTrigger -SqlInstance ComputerA\sql987 + +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. + +.EXAMPLE +Get-DbaTrigger -SqlInstance 'ComputerA\sql987','ComputerB' + +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified from two instances. + +.EXAMPLE +Get-DbaTrigger -SqlInstance ComputerA\sql987 | Out-Gridview + +Returns a gridview displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. + +.EXAMPLE +'ComputerA\sql987','ComputerB' | Get-DbaTrigger | Out-Gridview + +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified from two instances. + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "instance")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase + ) + + process { + foreach ($Instance in $SqlInstance) { + Write-Verbose "Connecting to $Instance" + try { + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential -Erroraction SilentlyContinue + } + catch { + Write-Warning "Can't connect to $Instance" + continue + } + + Write-Verbose "Getting Server Level Triggers on $Instance" + $server.Triggers | + ForEach-Object { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TriggerLevel = "Server" + Database = $null + TriggerName = $_.Name + Status = switch ($_.IsEnabled) { $true { "Enabled" } $false { "Disabled" } } + DateLastModified = $_.DateLastModified + } + } + + Write-Verbose "Getting Database Level Triggers on $Instance" + $dbs = $server.Databases | Where-Object { $_.status -eq 'Normal' } + + if ($Database) { + $dbs = $dbs | Where-Object Name -in $Database + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -notin $ExcludeDatabase + } + + $dbs | + ForEach-Object { + $DatabaseName = $_.Name + Write-Verbose "Getting Database Level Triggers on Database $DatabaseName on $Instance" + $_.Triggers | + ForEach-Object { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TriggerLevel = "Database" + Database = $DatabaseName + TriggerName = $_.Name + Status = switch ($_.IsEnabled) { $true { "Enabled" } $false { "Disabled" } } + DateLastModified = $_.DateLastModified + } + } + } + } + } +} +function Get-DbaUptime { + <# + .SYNOPSIS + Returns the uptime of the SQL Server instance, and if required the hosting windows server + + .DESCRIPTION + By default, this command returns for each SQL Server instance passed in: + SQL Instance last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string + Hosting Windows server last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string + + .PARAMETER SqlInstance + The SQL Server instance that you're connecting to. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). To use: + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. + + To connect to SQL Server as a different Windows user, run PowerShell as that user. + + .PARAMETER Credential + Allows you to login to the computer (not SQL Server instance) using alternative Windows credentials. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CIM + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaUptime + + .EXAMPLE + Get-DbaUptime -SqlInstance SqlBox1\Instance2 + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on winserver + + .EXAMPLE + Get-DbaUptime -SqlInstance winserver\sqlexpress, sql2016 + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on host winserver and the default instance on host sql2016 + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaUptime + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for every server listed in the Central Management Server on sql2014 + + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $nowutc = (Get-Date).ToUniversalTime() + } + process { + foreach ($instance in $SqlInstance) { + if ($instance.Gettype().FullName -eq [System.Management.Automation.PSCustomObject] ) { + $servername = $instance.SqlInstance + } + elseif ($instance.Gettype().FullName -eq [Microsoft.SqlServer.Management.Smo.Server]) { + $servername = $instance.NetName + } + else { + $servername = $instance.ComputerName; + } + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + Write-Message -Level Verbose -Message "Getting start times for $servername" + #Get tempdb creation date + [dbadatetime]$SQLStartTime = $server.Databases["tempdb"].CreateDate + $SQLUptime = New-TimeSpan -Start $SQLStartTime.ToUniversalTime() -End $nowutc + $SQLUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($SQLUptime.Days), $($SQLUptime.Hours), $($SQLUptime.Minutes), $($SQLUptime.Seconds) + + $WindowsServerName = (Resolve-DbaNetworkName $servername -Credential $Credential).FullComputerName + + try { + Write-Message -Level Verbose -Message "Getting WinBootTime via CimInstance for $servername" + $WinBootTime = (Get-DbaOperatingSystem -ComputerName $windowsServerName -Credential $Credential -ErrorAction SilentlyContinue).LastBootTime + $WindowsUptime = New-TimeSpan -start $WinBootTime.ToUniversalTime() -end $nowutc + $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) + } + catch { + try { + Write-Message -Level Verbose -Message "Getting WinBootTime via CimInstance DCOM" + $CimOption = New-CimSessionOption -Protocol DCOM + $CimSession = New-CimSession -Credential:$Credential -ComputerName $WindowsServerName -SessionOption $CimOption + [dbadatetime]$WinBootTime = ($CimSession | Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime + $WindowsUptime = New-TimeSpan -start $WinBootTime.ToUniversalTime() -end $nowutc + $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) + } + catch { + Stop-Function -Message "Failure getting WinBootTime" -ErrorRecord $_ -Target $instance -Continue + } + } + + [PSCustomObject]@{ + ComputerName = $WindowsServerName + InstanceName = $server.ServiceName + SqlServer = $server.Name + SqlUptime = $SQLUptime + WindowsUptime = $WindowsUptime + SqlStartTime = $SQLStartTime + WindowsBootTime = $WinBootTime + SinceSqlStart = $SQLUptimeString + SinceWindowsBoot = $WindowsUptimeString + } + } + } +} +function Get-DbaUserLevelPermission { + <# + .SYNOPSIS + Displays detailed permissions information for the server and database roles and securables. + + .DESCRIPTION + This command will display all server logins, server level securable, database logins and database securables. + + DISA STIG implementators will find this command useful as it uses Permissions.sql provided by DISA. + + Note that if you Ctrl-C out of this command and end it prematurely, it will leave behind a STIG schema in tempdb. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER ExcludeSystemDatabase + Allows you to suppress output on system databases + + .PARAMETER IncludePublicGuest + Allows you to include output for public and guest grants. + + .PARAMETER IncludeSystemObjects + Allows you to include output on sys schema objects. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Discovery, Permissions, Security + Author: Brandon Abshire, netnerds.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaUserLevelPermission + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008, sqlserver2012 + Check server and database permissions for servers sql2008 and sqlserver2012. + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008 -Database TestDB + Check server and database permissions on server sql2008 for only the TestDB database + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008 -Database TestDB -IncludePublicGuest -IncludeSystemObjects + Check server and database permissions on server sql2008 for only the TestDB database, + including public and guest grants, and sys schema objects. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Position = 1, Mandatory = $false)] + [switch]$ExcludeSystemDatabase, + [switch]$IncludePublicGuest, + [switch]$IncludeSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + + $sql = [System.IO.File]::ReadAllText("$script:PSModuleRoot\bin\stig.sql") + + $endSQL = " BEGIN TRY DROP FUNCTION STIG.server_effective_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.server_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.members_of_server_role END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.server_roles_of END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.server_role_members END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.database_effective_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.database_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.members_of_db_role END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.database_roles_of END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.database_role_members END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP SCHEMA STIG END TRY BEGIN CATCH END CATCH; + GO" + + + $serverSQL = "SELECT 'SERVER LOGINS' AS Type , + sl.name AS Member , + ISNULL(srm.role, 'None') AS [Role/Securable/Class] , + ' ' AS [Schema/Owner] , + ' ' AS [Securable] , + ' ' AS [Grantee Type] , + ' ' AS [Grantee] , + ' ' AS [Permission] , + ' ' AS [State] , + ' ' AS [Grantor] , + ' ' AS [Grantor Type] , + ' ' AS [Source View] + FROM master.sys.syslogins sl + LEFT JOIN tempdb.[STIG].[server_role_members] srm ON sl.name = srm.member + WHERE sl.name NOT LIKE 'NT %' + AND sl.name NOT LIKE '##%' + UNION + SELECT 'SERVER SECURABLES' AS Type , + sl.name , + sp.[Securable Class] COLLATE SQL_Latin1_General_CP1_CI_AS , + ' ' , + sp.[Securable] , + sp.[Grantee Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.Grantee , + sp.Permission COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.State COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.Grantor , + sp.[Grantor Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.[Source View] + FROM master.sys.syslogins sl + LEFT JOIN tempdb.[STIG].[server_permissions] sp ON sl.name = sp.Grantee + WHERE sl.name NOT LIKE 'NT %' + AND sl.name NOT LIKE '##%';" + + $dbSQL = "SELECT 'DB ROLE MEMBERS' AS type , + Member , + Role , + ' ' AS [Schema/Owner] , + ' ' AS [Securable] , + ' ' AS [Grantee Type] , + ' ' AS [Grantee] , + ' ' AS [Permission] , + ' ' AS [State] , + ' ' AS [Grantor] , + ' ' AS [Grantor Type] , + ' ' AS [Source View] + FROM tempdb.[STIG].[database_role_members] + UNION + SELECT DISTINCT + 'DB SECURABLES' AS Type , + drm.member , + dp.[Securable Type or Class] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.[Schema/Owner] , + dp.Securable , + dp.[Grantee Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.Grantee , + dp.Permission COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.State COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.Grantor , + dp.[Grantor Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.[Source View] + FROM tempdb.[STIG].[database_role_members] drm + LEFT JOIN tempdb.[STIG].[database_permissions] dp ON ( drm.member = dp.grantee + OR drm.role = dp.grantee + ) + WHERE dp.Grantor IS NOT NULL + AND [Schema/Owner] <> 'sys'" + + if ($IncludePublicGuest) { $dbSQL = $dbSQL.Replace("LEFT JOIN", "FULL JOIN") } + if ($IncludeSystemObjects) { $dbSQL = $dbSQL.Replace("AND [Schema/Owner] <> 'sys'", "") } + + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($ExcludeSystemDatabase) { + $dbs = $dbs | Where-Object IsSystemObject -eq $false + } + + #reset $serverDT + $serverDT = $null + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "The database $db is not accessible" -Continue + } + + $sql = $sql.Replace("", $db.Name) + + #Create objects in active database + Write-Message -Level Verbose -Message "Creating objects" + try { $db.ExecuteNonQuery($sql) } catch {} # sometimes it complains about not being able to drop the stig schema if the person Ctrl-C'd before. + + #Grab permissions data + if (-not $serverDT) { + Write-Message -Level Verbose -Message "Building data table for server objects" + + try { $serverDT = $db.Query($serverSQL) } catch { } + + foreach ($row in $serverDT) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Object = 'SERVER' + Type = $row.Type + Member = $row.Member + RoleSecurableClass = $row.'Role/Securable/Class' + SchemaOwner = $row.'Schema/Owner' + Securable = $row.Securable + GranteeType = $row.'Grantee Type' + Grantee = $row.Grantee + Permission = $row.Permission + State = $row.State + Grantor = $row.Grantor + GrantorType = $row.'Grantor Type' + SourceView = $row.'Source View' + } + } + } + + Write-Message -Level Verbose -Message "Building data table for $db objects" + try { $dbDT = $db.Query($dbSQL) } catch { } + + foreach ($row in $dbDT) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Object = $db.Name + Type = $row.Type + Member = $row.Member + RoleSecurableClass = $row.'Role/Securable/Class' + SchemaOwner = $row.'Schema/Owner' + Securable = $row.Securable + GranteeType = $row.'Grantee Type' + Grantee = $row.Grantee + Permission = $row.Permission + State = $row.State + Grantor = $row.Grantor + GrantorType = $row.'Grantor Type' + SourceView = $row.'Source View' + } + } + + #Delete objects + Write-Message -Level Verbose -Message "Deleting objects" + try { $db.ExecuteNonQuery($endSQL) } catch { } + $sql = $sql.Replace($db.Name, "") + + #Sashay Away + } + } + } +} +function Get-DbaWaitingTask { + <# + .SYNOPSIS + Displays waiting task. + + .DESCRIPTION + This command is based on waiting task T-SQL script published by Paul Randal. + Reference: https://www.sqlskills.com/blogs/paul/updated-sys-dm_os_waiting_tasks-script-2/ + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version XXXX or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Spid + Find the waiting task of one or more specific process ids + + .PARAMETER IncludeSystemSpid + If this switch is enabled, the output will include the system sessions. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Waits,Task,WaitTask + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWaitingTask + + .EXAMPLE + Get-DbaWaitingTask -SqlInstance sqlserver2014a + + Returns the waiting task for all sessions on sqlserver2014a + + .EXAMPLE + Get-DbaWaitingTask -SqlInstance sqlserver2014a -IncludeSystemSpid + + Returns the waiting task for all sessions (user and system) on sqlserver2014a + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ValueFromPipelineByPropertyName = $true)] + [object[]]$Spid, + [switch]$IncludeSystemSpid, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = " + SELECT + [owt].[session_id] AS [Spid], + [owt].[exec_context_id] AS [Thread], + [ot].[scheduler_id] AS [Scheduler], + [owt].[wait_duration_ms] AS [WaitMs], + [owt].[wait_type] AS [WaitType], + [owt].[blocking_session_id] AS [BlockingSpid], + [owt].[resource_description] AS [ResourceDesc], + CASE [owt].[wait_type] + WHEN N'CXPACKET' THEN + RIGHT ([owt].[resource_description], + CHARINDEX (N'=', REVERSE ([owt].[resource_description])) - 1) + ELSE NULL + END AS [NodeId], + [eqmg].[dop] AS [Dop], + [er].[database_id] AS [DbId], + [est].text AS [SqlText], + [eqp].[query_plan] AS [QueryPlan], + CAST ('https://www.sqlskills.com/help/waits/' + [owt].[wait_type] as XML) AS [URL] + FROM sys.dm_os_waiting_tasks [owt] + INNER JOIN sys.dm_os_tasks [ot] ON + [owt].[waiting_task_address] = [ot].[task_address] + INNER JOIN sys.dm_exec_sessions [es] ON + [owt].[session_id] = [es].[session_id] + INNER JOIN sys.dm_exec_requests [er] ON + [es].[session_id] = [er].[session_id] + FULL JOIN sys.dm_exec_query_memory_grants [eqmg] ON + [owt].[session_id] = [eqmg].[session_id] + OUTER APPLY sys.dm_exec_sql_text ([er].[sql_handle]) [est] + OUTER APPLY sys.dm_exec_query_plan ([er].[plan_handle]) [eqp] + WHERE + [es].[is_user_process] = $(if (Test-Bound 'IncludeSystemSpid') {0} else {1}) + ORDER BY + [owt].[session_id], + [owt].[exec_context_id] + OPTION(RECOMPILE);" + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $results = $server.Query($sql) + foreach ($row in $results) { + if (Test-Bound 'Spid') { + if ($row.Spid -notin $Spid) { continue } + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Spid = $row.Spid + Thread = $row.Thread + Scheduler = $row.Scheduler + WaitMs = $row.WaitMs + WaitType = $row.WaitType + BlockingSpid = $row.BlockingSpid + ResourceDesc = $row.ResourceDesc + NodeId = $row.NodeId + Dop = $row.Dop + DbId = $row.DbId + SqlText = $row.SqlText + QueryPlan = $row.QueryPlan + InfoUrl = $row.InfoUrl + } | Select-DefaultView -ExcludeProperty 'SqlText', 'QueryPlan', 'InfoUrl' + } + } + } +} +function Get-DbaWaitResource { + <# + .SYNOPSIS + Returns the resource being waited upon + + .DESCRIPTION + Given a wait resource in the form of: + 'PAGE: 10:1:9180084 ' + returns the database, data file and the system object which is being waited up. + Given a wait resource in the form of: + 'KEY: 7:35457594073541168 (de21f92a1572)' + returns the database, object and index that is being waited on, With the -row switch the row data will also be returned. + .PARAMETER SqlInstance + The SQL Server instance to restore to. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER WaitResource + The waitresource value as supplied in sys.dm_exec_requests + + .PARAMETER Row + If this switch provided also returns the value of the row being waited on with KEY wait resources + + .PARAMETER EnableException + Replaces user friendly yellow warnings with bloody red exceptions of doom! + Use this if you want the function to throw terminating errors you want to catch. + + .EXAMPLE + Get-DbaWaitResource -SqlInstance server1 -WaitResource 'PAGE: 10:1:9180084' + + Will return an object containing; database name, data file name, schema name and the object which owns the resource + + .EXAMPLE + Get-DbaWaitResource -Sql Instance server2 -WaitResource 'KEY: 7:35457594073541168 (de21f92a1572)' + + Will return an object containing; database name, schema name and index name which is being waited on. + + .EXAMPLE + Get-DbaWaitResource -Sql Instance server2 -WaitResource 'KEY: 7:35457594073541168 (de21f92a1572)' -row + + Will return an object containing; database name, schema name and index name which is being waited on, and in addition the contents of the locked row at the time the command is run. + + .NOTES + Tags: Pages, DBCC + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$WaitResource, + [switch]$Row, + [switch]$EnableException + ) + + process { + if ($WaitResource -notmatch '^PAGE: [0-9]*:[0-9]*:[0-9]*$' -and $WaitResource -notmatch '^KEY: [0-9]*:[0-9]* \([a-f0-9]*\)$'){ + Stop-Function -Message "Row input - $WaitResource - Improperly formatted" + return + } + + try { + $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Cannot connect to $SqlInstance" + } + + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):*' + $ResourceType = $matches.Type + $DbId = $matches.DbId + $DbName = ($server.Databases | Where-Object ID -eq $dbid).Name + if ($null -eq $DbName){ + stop-function -Message "Database with id $dbid does not exist on $server" + return + } + if ($ResourceType -eq 'PAGE'){ + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):(?[0-9]*):(?[0-9]*)$' + $DataFileSql = "select name, physical_name from sys.master_files where database_id=$DbID and file_ID=$($matches.FileID);" + $DataFile = $server.query($DataFileSql) + if ($null -eq $DataFile){ + Write-Message -Level Warning -Message "Datafile with id $($matches.FileID) for $dbname not found" + return + } + $ObjectIdSQL = "dbcc traceon (3604); dbcc page ($dbid,$($matches.fileID),$($matches.PageID),2) with tableresults;" + try { + $ObjectID = ($server.databases[$dbname].Query($ObjectIdSQL) | Where-Object Field -eq 'Metadata: ObjectId').Value + } + catch { + Stop-Function -Message "You've requested a page beyond the end of the database, exiting" + return + } + if ($null -eq $ObjectID){ + Write-Message -Level Warning -Message "Object not found, could have been delete, or a transcription error when copying the Wait_resource to PowerShell" + return + } + $ObjectSql = "select SCHEMA_NAME(schema_id) as SchemaName, name, type_desc from sys.all_objects where object_id=$objectID;" + $Object = $server.databases[$dbname].query($ObjectSql) + if ($null -eq $Object){ + Write-Message -Warning "Object could not be found. Could have been removed, or could be a transcription error copying the Wait_resource to sowerShell" + } + [PsCustomObject]@{ + DatabaseID = $DbId + DatabaseName = $DbName + DataFileName = $Datafile.name + DataFilePath = $DataFile.physical_name + ObjectID = $ObjectID + ObjectName = $Object.Name + ObjectSchema = $Object.SchemaName + ObjectType = $Object.type_desc + } + } + if ($ResourceType -eq 'KEY'){ + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):(?[0-9]*) (?\(.*\))$' + $IndexSql = "select + sp.object_id as ObjectID, + OBJECT_SCHEMA_NAME(sp.object_id) as SchemaName, + sao.name as ObjectName, + si.name as IndexName + from + sys.partitions sp inner join sys.indexes si on sp.index_id=si.index_id and sp.object_id=si.object_id + inner join sys.all_objects sao on sp.object_id=sao.object_id + where + hobt_id = $($matches.frodo); + " + $Index = $server.databases[$dbname].Query($IndexSql) + if ($null -eq $Index){ + Write-Message -Level Warning -Message "Heap or B-Tree with ID $($matches.frodo) can not be found in $dbname on $server" + return + } + $output = [PsCustomObject]@{ + DatabaseID = $DbId + DatabaseName = $DbName + SchemaName = $Index.SchemaName + IndexName = $Index.IndexName + ObjectID = $index.ObjectID + Objectname = $index.ObjectName + HobtID = $matches.frodo + } + if ($row -eq $True){ + $DataSql = "select * from $($Index.SchemaName).$($Index.ObjectName) with (NOLOCK) where %%lockres%% ='$($matches.physloc)'" + $Data = $server.databases[$dbname].query($DataSql) + if ($null -eq $data){ + Write-Message -Level warning -Message "Could not retrieve the data. It may have been deleted or moved since the wait resource value was generated" + } + else{ + $output | Add-Member -Type NoteProperty -Name ObjectData -Value $Data + $output | Select-Object * -ExpandProperty ObjectData + } + } + else { + $output + } + } + } +} +function Get-DbaWaitStatistic { + <# + .SYNOPSIS + Displays wait statistics + + .DESCRIPTION + This command is based off of Paul Randal's post "Wait statistics, or please tell me where it hurts" + + Returns: + WaitType + Category + WaitSeconds + ResourceSeconds + SignalSeconds + WaitCount + Percentage + AverageWaitSeconds + AverageResourceSeconds + AverageSignalSeconds + URL + + Reference: https://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/ + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Threshold + Threshold, in percentage of all waits on the system. Default per Paul's post is 95%. + + .PARAMETER IncludeIgnorable + Some waits are no big deal and can be safely ignored in most circumstances. If you've got weird issues with mirroring or AGs. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: WaitStatistic + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWaitStatistic + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008, sqlserver2012 + + Check wait statistics for servers sql2008 and sqlserver2012 + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008 -Threshold 98 -IncludeIgnorable + + Check wait statistics on server sql2008 for thresholds above 98% and include wait stats that are most often, but not always, ignorable + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008 | Select * + + Shows detailed notes, if available, from Paul's post + + .EXAMPLE + $output = Get-DbaWaitStatistic -SqlInstance sql2008 -Threshold 100 -IncludeIgnorable | Select * | ConvertTo-DbaDataTable + + Collects all Wait Statistics (including ignorable waits) on server sql2008 into a Data Table. + + + .EXAMPLE + $output = Get-DbaWaitStatistic -SqlInstance sql2008 + $output + foreach ($row in ($output | Sort-Object -Unique Url)) { Start-Process ($row).Url } + + Displays the output then loads the associated sqlskills website for each result. Opens one tab per unique URL. + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int]$Threshold = 95, + [switch]$IncludeIgnorable, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $details = [pscustomobject]@{ + CXPACKET = "This indicates parallelism, not necessarily that there's a problem. The coordinator thread in a parallel query always accumulates these waits. If the parallel threads are not given equal amounts of work to do, or one thread blocks, the waiting threads will also accumulate CXPACKET waits, which will make them aggregate a lot faster - this is a problem. One thread may have a lot more to do than the others, and so the whole query is blocked while the long-running thread completes. If this is combined with a high number of PAGEIOLATCH_XX waits, it could be large parallel table scans going on because of incorrect non-clustered indexes, or a bad query plan. If neither of these are the issue, you might want to try setting MAXDOP to 4, 2, or 1 for the offending queries (or possibly the whole instance). Make sure that if you have a NUMA system that you try setting MAXDOP to the number of cores in a single NUMA node first to see if that helps the problem. You also need to consider the MAXDOP effect on a mixed-load system. Play with the cost threshold for parallelism setting (bump it up to, say, 25) before reducing the MAXDOP of the whole instance. And don't forget Resource Governor in Enterprise Edition of SQL Server 2008 onward that allows DOP governing for a particular group of connections to the server." + PAGEIOLATCH_XX = "This is where SQL Server is waiting for a data page to be read from disk into memory. It may indicate a bottleneck at the IO subsystem level (which is a common knee-jerk response to seeing these), but why is the I/O subsystem having to service so many reads? It could be buffer pool/memory pressure (i.e. not enough memory for the workload), a sudden change in query plans causing a large parallel scan instead of a seek, plan cache bloat, or a number of other things. Don't assume the root cause is the I/O subsystem." + ASYNC_NETWORK_IO = "This is usually where SQL Server is waiting for a client to finish consuming data. It could be that the client has asked for a very large amount of data or just that it's consuming it reeeeeally slowly because of poor programming – I rarely see this being a network issue. Clients often process one row at a time – called RBAR or Row-By-Agonizing-Row – instead of caching the data on the client and acknowledging to SQL Server immediately." + WRITELOG = "This is the log management system waiting for a log flush to disk. It commonly indicates that the I/O subsystem can't keep up with the log flush volume, but on very high-volume systems it could also be caused by internal log flush limits, that may mean you have to split your workload over multiple databases or even make your transactions a little longer to reduce log flushes. To be sure it is the I/O subsystem, use the DMV sys.dm_io_virtual_file_stats to examine the I/O latency for the log file and see if it correlates to the average WRITELOG time. If WRITELOG is longer, you've got internal contention and need to shard. If not, investigate why you're creating so much transaction log." + BROKER_RECEIVE_WAITFOR = "This is just Service Broker waiting around for new messages to receive. I would add this to the list of waits to filter out and re-run the wait stats query." + MSQL_XP = "This is SQL Server waiting for an extended stored-proc to finish. This could indicate a problem in your XP code." + OLEDB = "As its name suggests, this is a wait for something communicating using OLEDB – e.g. a linked server. However, OLEDB is also used by all DMVs and by DBCC CHECKDB, so don't assume linked servers are the problem – it could be a third-party monitoring tool making excessive DMV calls. If it *is* a linked server (wait times in the 10s or 100s of milliseconds), go to the linked server and do wait stats analysis there to figure out what the performance issue is there." + BACKUPIO = "This can show up when you're backing up to a slow I/O subsystem, like directly to tape, which is slooooow, or over a network." + LCK_M_XX = "This is simply the thread waiting for a lock to be granted and indicates blocking problems. These could be caused by unwanted lock escalation or bad programming, but could also be from I/Os taking a long time causing locks to be held for longer than usual. Look at the resource associated with the lock using the DMV sys.dm_os_waiting_tasks. Don't assume that locking is the root cause." + ONDEMAND_TASK_QUEUE = "This is normal and is part of the background task system (e.g. deferred drop, ghost cleanup). I would add this to the list of waits to filter out and re-run the wait stats query." + BACKUPBUFFER = "This commonly show up with BACKUPIO and is a backup thread waiting for a buffer to write backup data into." + IO_COMPLETION = "This is SQL Server waiting for non-data page I/Os to complete and could be an indication that the I/O subsystem is overloaded if the latencies look high (see Are I/O latencies killing your performance?)" + SOS_SCHEDULER_YIELD = "This is code running that doesn't hit any resource waits." + DBMIRROR_EVENTS_QUEUE = "These two are database mirroring just sitting around waiting for something to do. I would add these to the list of waits to filter out and re-run the wait stats query." + DBMIRRORING_CMD = "These two are database mirroring just sitting around waiting for something to do. I would add these to the list of waits to filter out and re-run the wait stats query." + PAGELATCH_XX = "This is contention for access to in-memory copies of pages. The most well-known cases of these are the PFS and SGAM contention that can occur in tempdb under certain workloads. To find out what page the contention is on, you'll need to use the DMV sys.dm_os_waiting_tasks to figure out what page the latch is for. For tempdb issues, Robert Davis (blog | twitter) has a good post showing how to do this. Another common cause I've seen is an index hot-spot with concurrent inserts into an index with an identity value key." + LATCH_XX = "This is contention for some non-page structure inside SQL Server – so not related to I/O or data at all. These can be hard to figure out and you're going to be using the DMV sys.dm_os_latch_stats. More on this in my Latches category." + PREEMPTIVE_OS_PIPEOPS = "This is SQL Server switching to preemptive scheduling mode to call out to Windows for something, and this particular wait is usually from using xp_cmdshell. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + THREADPOOL = "This says that there aren't enough worker threads on the system to satisfy demand. Commonly this is large numbers of high-DOP queries trying to execute and taking all the threads from the thread pool." + BROKER_TRANSMITTER = "This is just Service Broker waiting around for new messages to send. I would add this to the list of waits to filter out and re-run the wait stats query." + SQLTRACE_WAIT_ENTRIES = "Part of SQL Trace. I would add this to the list of waits to filter out and re-run the wait stats query." + DBMIRROR_DBM_MUTEX = "This one is undocumented and is contention for the send buffer that database mirroring shares between all the mirroring sessions on a server. It could indicate that you've got too many mirroring sessions." + RESOURCE_SEMAPHORE = "This is queries waiting for execution memory (the memory used to process the query operators – like a sort). This could be memory pressure or a very high concurrent workload." + PREEMPTIVE_OS_AUTHENTICATIONOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_GENERICOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + SLEEP_BPOOL_FLUSH = "This is normal to see and indicates that checkpoint is throttling itself to avoid overloading the IO subsystem. I would add this to the list of waits to filter out and re-run the wait stats query." + MSQL_DQ = "This is SQL Server waiting for a distributed query to finish. This could indicate a problem with the distributed query, or it could just be normal." + RESOURCE_SEMAPHORE_QUERY_COMPILE = "When there are too many concurrent query compilations going on, SQL Server will throttle them. I don't remember the threshold, but this can indicate excessive recompilation, or maybe single-use plans." + DAC_INIT = "This is the Dedicated Admin Connection initializing." + MSSEARCH = "This is normal to see for full-text operations. If this is the highest wait, it could mean your system is spending most of its time doing full-text queries. You might want to consider adding this to the filter list." + PREEMPTIVE_OS_FILEOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_LIBRARYOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_LOOKUPACCOUNTSID = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_QUERYREGISTRY = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + SQLTRACE_LOCK = "Part of SQL Trace. I would add this to the list of waits to filter out and re-run the wait stats query." + } + + # Thanks Brentg Ozar via https://gist.github.com/BrentOzar/42e82ee0603a1917c17d74c3fca26d34 + # Thanks Marcin Gminski‏ via https://www.dropbox.com/s/x3zr7u18tc1ojey/WaitStats.sql?dl=0 + + $category = [pscustomobject]@{ + ASYNC_IO_COMPLETION = 'Other Disk IO' + ASYNC_NETWORK_IO = 'Network IO' + BACKUPIO = 'Other Disk IO' + BROKER_CONNECTION_RECEIVE_TASK = 'Service Broker' + BROKER_DISPATCHER = 'Service Broker' + BROKER_ENDPOINT_STATE_MUTEX = 'Service Broker' + BROKER_EVENTHANDLER = 'Service Broker' + BROKER_FORWARDER = 'Service Broker' + BROKER_INIT = 'Service Broker' + BROKER_MASTERSTART = 'Service Broker' + BROKER_RECEIVE_WAITFOR = 'User Wait' + BROKER_REGISTERALLENDPOINTS = 'Service Broker' + BROKER_SERVICE = 'Service Broker' + BROKER_SHUTDOWN = 'Service Broker' + BROKER_START = 'Service Broker' + BROKER_TASK_SHUTDOWN = 'Service Broker' + BROKER_TASK_STOP = 'Service Broker' + BROKER_TASK_SUBMIT = 'Service Broker' + BROKER_TO_FLUSH = 'Service Broker' + BROKER_TRANSMISSION_OBJECT = 'Service Broker' + BROKER_TRANSMISSION_TABLE = 'Service Broker' + BROKER_TRANSMISSION_WORK = 'Service Broker' + BROKER_TRANSMITTER = 'Service Broker' + CHECKPOINT_QUEUE = 'Idle' + CHKPT = 'Tran Log IO' + CLR_AUTO_EVENT = 'SQL CLR' + CLR_CRST = 'SQL CLR' + CLR_JOIN = 'SQL CLR' + CLR_MANUAL_EVENT = 'SQL CLR' + CLR_MEMORY_SPY = 'SQL CLR' + CLR_MONITOR = 'SQL CLR' + CLR_RWLOCK_READER = 'SQL CLR' + CLR_RWLOCK_WRITER = 'SQL CLR' + CLR_SEMAPHORE = 'SQL CLR' + CLR_TASK_START = 'SQL CLR' + CLRHOST_STATE_ACCESS = 'SQL CLR' + CMEMPARTITIONED = 'Memory' + CMEMTHREAD = 'Memory' + CXPACKET = 'Parallelism' + DBMIRROR_DBM_EVENT = 'Mirroring' + DBMIRROR_DBM_MUTEX = 'Mirroring' + DBMIRROR_EVENTS_QUEUE = 'Mirroring' + DBMIRROR_SEND = 'Mirroring' + DBMIRROR_WORKER_QUEUE = 'Mirroring' + DBMIRRORING_CMD = 'Mirroring' + DTC = 'Transaction' + DTC_ABORT_REQUEST = 'Transaction' + DTC_RESOLVE = 'Transaction' + DTC_STATE = 'Transaction' + DTC_TMDOWN_REQUEST = 'Transaction' + DTC_WAITFOR_OUTCOME = 'Transaction' + DTCNEW_ENLIST = 'Transaction' + DTCNEW_PREPARE = 'Transaction' + DTCNEW_RECOVERY = 'Transaction' + DTCNEW_TM = 'Transaction' + DTCNEW_TRANSACTION_ENLISTMENT = 'Transaction' + DTCPNTSYNC = 'Transaction' + EE_PMOLOCK = 'Memory' + EXCHANGE = 'Parallelism' + EXTERNAL_SCRIPT_NETWORK_IOF = 'Network IO' + FCB_REPLICA_READ = 'Replication' + FCB_REPLICA_WRITE = 'Replication' + FT_COMPROWSET_RWLOCK = 'Full Text Search' + FT_IFTS_RWLOCK = 'Full Text Search' + FT_IFTS_SCHEDULER_IDLE_WAIT = 'Idle' + FT_IFTSHC_MUTEX = 'Full Text Search' + FT_IFTSISM_MUTEX = 'Full Text Search' + FT_MASTER_MERGE = 'Full Text Search' + FT_MASTER_MERGE_COORDINATOR = 'Full Text Search' + FT_METADATA_MUTEX = 'Full Text Search' + FT_PROPERTYLIST_CACHE = 'Full Text Search' + FT_RESTART_CRAWL = 'Full Text Search' + 'FULLTEXT GATHERER' = 'Full Text Search' + HADR_AG_MUTEX = 'Replication' + HADR_AR_CRITICAL_SECTION_ENTRY = 'Replication' + HADR_AR_MANAGER_MUTEX = 'Replication' + HADR_AR_UNLOAD_COMPLETED = 'Replication' + HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST = 'Replication' + HADR_BACKUP_BULK_LOCK = 'Replication' + HADR_BACKUP_QUEUE = 'Replication' + HADR_CLUSAPI_CALL = 'Replication' + HADR_COMPRESSED_CACHE_SYNC = 'Replication' + HADR_CONNECTIVITY_INFO = 'Replication' + HADR_DATABASE_FLOW_CONTROL = 'Replication' + HADR_DATABASE_VERSIONING_STATE = 'Replication' + HADR_DATABASE_WAIT_FOR_RECOVERY = 'Replication' + HADR_DATABASE_WAIT_FOR_RESTART = 'Replication' + HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING = 'Replication' + HADR_DB_COMMAND = 'Replication' + HADR_DB_OP_COMPLETION_SYNC = 'Replication' + HADR_DB_OP_START_SYNC = 'Replication' + HADR_DBR_SUBSCRIBER = 'Replication' + HADR_DBR_SUBSCRIBER_FILTER_LIST = 'Replication' + HADR_DBSEEDING = 'Replication' + HADR_DBSEEDING_LIST = 'Replication' + HADR_DBSTATECHANGE_SYNC = 'Replication' + HADR_FABRIC_CALLBACK = 'Replication' + HADR_FILESTREAM_BLOCK_FLUSH = 'Replication' + HADR_FILESTREAM_FILE_CLOSE = 'Replication' + HADR_FILESTREAM_FILE_REQUEST = 'Replication' + HADR_FILESTREAM_IOMGR = 'Replication' + HADR_FILESTREAM_IOMGR_IOCOMPLETION = 'Replication' + HADR_FILESTREAM_MANAGER = 'Replication' + HADR_FILESTREAM_PREPROC = 'Replication' + HADR_GROUP_COMMIT = 'Replication' + HADR_LOGCAPTURE_SYNC = 'Replication' + HADR_LOGCAPTURE_WAIT = 'Replication' + HADR_LOGPROGRESS_SYNC = 'Replication' + HADR_NOTIFICATION_DEQUEUE = 'Replication' + HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS = 'Replication' + HADR_NOTIFICATION_WORKER_STARTUP_SYNC = 'Replication' + HADR_NOTIFICATION_WORKER_TERMINATION_SYNC = 'Replication' + HADR_PARTNER_SYNC = 'Replication' + HADR_READ_ALL_NETWORKS = 'Replication' + HADR_RECOVERY_WAIT_FOR_CONNECTION = 'Replication' + HADR_RECOVERY_WAIT_FOR_UNDO = 'Replication' + HADR_REPLICAINFO_SYNC = 'Replication' + HADR_SEEDING_CANCELLATION = 'Replication' + HADR_SEEDING_FILE_LIST = 'Replication' + HADR_SEEDING_LIMIT_BACKUPS = 'Replication' + HADR_SEEDING_SYNC_COMPLETION = 'Replication' + HADR_SEEDING_TIMEOUT_TASK = 'Replication' + HADR_SEEDING_WAIT_FOR_COMPLETION = 'Replication' + HADR_SYNC_COMMIT = 'Replication' + HADR_SYNCHRONIZING_THROTTLE = 'Replication' + HADR_TDS_LISTENER_SYNC = 'Replication' + HADR_TDS_LISTENER_SYNC_PROCESSING = 'Replication' + HADR_THROTTLE_LOG_RATE_GOVERNOR = 'Log Rate Governor' + HADR_TIMER_TASK = 'Replication' + HADR_TRANSPORT_DBRLIST = 'Replication' + HADR_TRANSPORT_FLOW_CONTROL = 'Replication' + HADR_TRANSPORT_SESSION = 'Replication' + HADR_WORK_POOL = 'Replication' + HADR_WORK_QUEUE = 'Replication' + HADR_XRF_STACK_ACCESS = 'Replication' + INSTANCE_LOG_RATE_GOVERNOR = 'Log Rate Governor' + IO_COMPLETION = 'Other Disk IO' + IO_QUEUE_LIMIT = 'Other Disk IO' + IO_RETRY = 'Other Disk IO' + LATCH_DT = 'Latch' + LATCH_EX = 'Latch' + LATCH_KP = 'Latch' + LATCH_NL = 'Latch' + LATCH_SH = 'Latch' + LATCH_UP = 'Latch' + LAZYWRITER_SLEEP = 'Idle' + LCK_M_BU = 'Lock' + LCK_M_BU_ABORT_BLOCKERS = 'Lock' + LCK_M_BU_LOW_PRIORITY = 'Lock' + LCK_M_IS = 'Lock' + LCK_M_IS_ABORT_BLOCKERS = 'Lock' + LCK_M_IS_LOW_PRIORITY = 'Lock' + LCK_M_IU = 'Lock' + LCK_M_IU_ABORT_BLOCKERS = 'Lock' + LCK_M_IU_LOW_PRIORITY = 'Lock' + LCK_M_IX = 'Lock' + LCK_M_IX_ABORT_BLOCKERS = 'Lock' + LCK_M_IX_LOW_PRIORITY = 'Lock' + LCK_M_RIn_NL = 'Lock' + LCK_M_RIn_NL_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_NL_LOW_PRIORITY = 'Lock' + LCK_M_RIn_S = 'Lock' + LCK_M_RIn_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_S_LOW_PRIORITY = 'Lock' + LCK_M_RIn_U = 'Lock' + LCK_M_RIn_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_U_LOW_PRIORITY = 'Lock' + LCK_M_RIn_X = 'Lock' + LCK_M_RIn_X_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_X_LOW_PRIORITY = 'Lock' + LCK_M_RS_S = 'Lock' + LCK_M_RS_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RS_S_LOW_PRIORITY = 'Lock' + LCK_M_RS_U = 'Lock' + LCK_M_RS_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RS_U_LOW_PRIORITY = 'Lock' + LCK_M_RX_S = 'Lock' + LCK_M_RX_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_S_LOW_PRIORITY = 'Lock' + LCK_M_RX_U = 'Lock' + LCK_M_RX_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_U_LOW_PRIORITY = 'Lock' + LCK_M_RX_X = 'Lock' + LCK_M_RX_X_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_X_LOW_PRIORITY = 'Lock' + LCK_M_S = 'Lock' + LCK_M_S_ABORT_BLOCKERS = 'Lock' + LCK_M_S_LOW_PRIORITY = 'Lock' + LCK_M_SCH_M = 'Lock' + LCK_M_SCH_M_ABORT_BLOCKERS = 'Lock' + LCK_M_SCH_M_LOW_PRIORITY = 'Lock' + LCK_M_SCH_S = 'Lock' + LCK_M_SCH_S_ABORT_BLOCKERS = 'Lock' + LCK_M_SCH_S_LOW_PRIORITY = 'Lock' + LCK_M_SIU = 'Lock' + LCK_M_SIU_ABORT_BLOCKERS = 'Lock' + LCK_M_SIU_LOW_PRIORITY = 'Lock' + LCK_M_SIX = 'Lock' + LCK_M_SIX_ABORT_BLOCKERS = 'Lock' + LCK_M_SIX_LOW_PRIORITY = 'Lock' + LCK_M_U = 'Lock' + LCK_M_U_ABORT_BLOCKERS = 'Lock' + LCK_M_U_LOW_PRIORITY = 'Lock' + LCK_M_UIX = 'Lock' + LCK_M_UIX_ABORT_BLOCKERS = 'Lock' + LCK_M_UIX_LOW_PRIORITY = 'Lock' + LCK_M_X = 'Lock' + LCK_M_X_ABORT_BLOCKERS = 'Lock' + LCK_M_X_LOW_PRIORITY = 'Lock' + LOGBUFFER = 'Tran Log IO' + LOGMGR = 'Tran Log IO' + LOGMGR_FLUSH = 'Tran Log IO' + LOGMGR_PMM_LOG = 'Tran Log IO' + LOGMGR_QUEUE = 'Idle' + LOGMGR_RESERVE_APPEND = 'Tran Log IO' + MEMORY_ALLOCATION_EXT = 'Memory' + MEMORY_GRANT_UPDATE = 'Memory' + MSQL_XACT_MGR_MUTEX = 'Transaction' + MSQL_XACT_MUTEX = 'Transaction' + MSSEARCH = 'Full Text Search' + NET_WAITFOR_PACKET = 'Network IO' + ONDEMAND_TASK_QUEUE = 'Idle' + PAGEIOLATCH_DT = 'Buffer IO' + PAGEIOLATCH_EX = 'Buffer IO' + PAGEIOLATCH_KP = 'Buffer IO' + PAGEIOLATCH_NL = 'Buffer IO' + PAGEIOLATCH_SH = 'Buffer IO' + PAGEIOLATCH_UP = 'Buffer IO' + PAGELATCH_DT = 'Buffer Latch' + PAGELATCH_EX = 'Buffer Latch' + PAGELATCH_KP = 'Buffer Latch' + PAGELATCH_NL = 'Buffer Latch' + PAGELATCH_SH = 'Buffer Latch' + PAGELATCH_UP = 'Buffer Latch' + POOL_LOG_RATE_GOVERNOR = 'Log Rate Governor' + PREEMPTIVE_ABR = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPMEDIA = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPTAPE = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPVDIDEVICE = 'Preemptive' + PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL = 'Preemptive' + PREEMPTIVE_COM_COCREATEINSTANCE = 'Preemptive' + PREEMPTIVE_COM_COGETCLASSOBJECT = 'Preemptive' + PREEMPTIVE_COM_CREATEACCESSOR = 'Preemptive' + PREEMPTIVE_COM_DELETEROWS = 'Preemptive' + PREEMPTIVE_COM_GETCOMMANDTEXT = 'Preemptive' + PREEMPTIVE_COM_GETDATA = 'Preemptive' + PREEMPTIVE_COM_GETNEXTROWS = 'Preemptive' + PREEMPTIVE_COM_GETRESULT = 'Preemptive' + PREEMPTIVE_COM_GETROWSBYBOOKMARK = 'Preemptive' + PREEMPTIVE_COM_LBFLUSH = 'Preemptive' + PREEMPTIVE_COM_LBLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_LBREADAT = 'Preemptive' + PREEMPTIVE_COM_LBSETSIZE = 'Preemptive' + PREEMPTIVE_COM_LBSTAT = 'Preemptive' + PREEMPTIVE_COM_LBUNLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_LBWRITEAT = 'Preemptive' + PREEMPTIVE_COM_QUERYINTERFACE = 'Preemptive' + PREEMPTIVE_COM_RELEASE = 'Preemptive' + PREEMPTIVE_COM_RELEASEACCESSOR = 'Preemptive' + PREEMPTIVE_COM_RELEASEROWS = 'Preemptive' + PREEMPTIVE_COM_RELEASESESSION = 'Preemptive' + PREEMPTIVE_COM_RESTARTPOSITION = 'Preemptive' + PREEMPTIVE_COM_SEQSTRMREAD = 'Preemptive' + PREEMPTIVE_COM_SEQSTRMREADANDWRITE = 'Preemptive' + PREEMPTIVE_COM_SETDATAFAILURE = 'Preemptive' + PREEMPTIVE_COM_SETPARAMETERINFO = 'Preemptive' + PREEMPTIVE_COM_SETPARAMETERPROPERTIES = 'Preemptive' + PREEMPTIVE_COM_STRMLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_STRMSEEKANDREAD = 'Preemptive' + PREEMPTIVE_COM_STRMSEEKANDWRITE = 'Preemptive' + PREEMPTIVE_COM_STRMSETSIZE = 'Preemptive' + PREEMPTIVE_COM_STRMSTAT = 'Preemptive' + PREEMPTIVE_COM_STRMUNLOCKREGION = 'Preemptive' + PREEMPTIVE_CONSOLEWRITE = 'Preemptive' + PREEMPTIVE_CREATEPARAM = 'Preemptive' + PREEMPTIVE_DEBUG = 'Preemptive' + PREEMPTIVE_DFSADDLINK = 'Preemptive' + PREEMPTIVE_DFSLINKEXISTCHECK = 'Preemptive' + PREEMPTIVE_DFSLINKHEALTHCHECK = 'Preemptive' + PREEMPTIVE_DFSREMOVELINK = 'Preemptive' + PREEMPTIVE_DFSREMOVEROOT = 'Preemptive' + PREEMPTIVE_DFSROOTFOLDERCHECK = 'Preemptive' + PREEMPTIVE_DFSROOTINIT = 'Preemptive' + PREEMPTIVE_DFSROOTSHARECHECK = 'Preemptive' + PREEMPTIVE_DTC_ABORT = 'Preemptive' + PREEMPTIVE_DTC_ABORTREQUESTDONE = 'Preemptive' + PREEMPTIVE_DTC_BEGINTRANSACTION = 'Preemptive' + PREEMPTIVE_DTC_COMMITREQUESTDONE = 'Preemptive' + PREEMPTIVE_DTC_ENLIST = 'Preemptive' + PREEMPTIVE_DTC_PREPAREREQUESTDONE = 'Preemptive' + PREEMPTIVE_FILESIZEGET = 'Preemptive' + PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION = 'Preemptive' + PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION = 'Preemptive' + PREEMPTIVE_FSAOLEDB_STARTTRANSACTION = 'Preemptive' + PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO = 'Preemptive' + PREEMPTIVE_GETRMINFO = 'Preemptive' + PREEMPTIVE_HADR_LEASE_MECHANISM = 'Preemptive' + PREEMPTIVE_HTTP_EVENT_WAIT = 'Preemptive' + PREEMPTIVE_HTTP_REQUEST = 'Preemptive' + PREEMPTIVE_LOCKMONITOR = 'Preemptive' + PREEMPTIVE_MSS_RELEASE = 'Preemptive' + PREEMPTIVE_ODBCOPS = 'Preemptive' + PREEMPTIVE_OLE_UNINIT = 'Preemptive' + PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN = 'Preemptive' + PREEMPTIVE_OLEDB_ABORTTRAN = 'Preemptive' + PREEMPTIVE_OLEDB_GETDATASOURCE = 'Preemptive' + PREEMPTIVE_OLEDB_GETLITERALINFO = 'Preemptive' + PREEMPTIVE_OLEDB_GETPROPERTIES = 'Preemptive' + PREEMPTIVE_OLEDB_GETPROPERTYINFO = 'Preemptive' + PREEMPTIVE_OLEDB_GETSCHEMALOCK = 'Preemptive' + PREEMPTIVE_OLEDB_JOINTRANSACTION = 'Preemptive' + PREEMPTIVE_OLEDB_RELEASE = 'Preemptive' + PREEMPTIVE_OLEDB_SETPROPERTIES = 'Preemptive' + PREEMPTIVE_OLEDBOPS = 'Preemptive' + PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE = 'Preemptive' + PREEMPTIVE_OS_AUTHENTICATIONOPS = 'Preemptive' + PREEMPTIVE_OS_AUTHORIZATIONOPS = 'Preemptive' + PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT = 'Preemptive' + PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID = 'Preemptive' + PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER = 'Preemptive' + PREEMPTIVE_OS_BACKUPREAD = 'Preemptive' + PREEMPTIVE_OS_CLOSEHANDLE = 'Preemptive' + PREEMPTIVE_OS_CLUSTEROPS = 'Preemptive' + PREEMPTIVE_OS_COMOPS = 'Preemptive' + PREEMPTIVE_OS_COMPLETEAUTHTOKEN = 'Preemptive' + PREEMPTIVE_OS_COPYFILE = 'Preemptive' + PREEMPTIVE_OS_CREATEDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_CREATEFILE = 'Preemptive' + PREEMPTIVE_OS_CRYPTACQUIRECONTEXT = 'Preemptive' + PREEMPTIVE_OS_CRYPTIMPORTKEY = 'Preemptive' + PREEMPTIVE_OS_CRYPTOPS = 'Preemptive' + PREEMPTIVE_OS_DECRYPTMESSAGE = 'Preemptive' + PREEMPTIVE_OS_DELETEFILE = 'Preemptive' + PREEMPTIVE_OS_DELETESECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_DEVICEIOCONTROL = 'Preemptive' + PREEMPTIVE_OS_DEVICEOPS = 'Preemptive' + PREEMPTIVE_OS_DIRSVC_NETWORKOPS = 'Preemptive' + PREEMPTIVE_OS_DISCONNECTNAMEDPIPE = 'Preemptive' + PREEMPTIVE_OS_DOMAINSERVICESOPS = 'Preemptive' + PREEMPTIVE_OS_DSGETDCNAME = 'Preemptive' + PREEMPTIVE_OS_DTCOPS = 'Preemptive' + PREEMPTIVE_OS_ENCRYPTMESSAGE = 'Preemptive' + PREEMPTIVE_OS_FILEOPS = 'Preemptive' + PREEMPTIVE_OS_FINDFILE = 'Preemptive' + PREEMPTIVE_OS_FLUSHFILEBUFFERS = 'Preemptive' + PREEMPTIVE_OS_FORMATMESSAGE = 'Preemptive' + PREEMPTIVE_OS_FREECREDENTIALSHANDLE = 'Preemptive' + PREEMPTIVE_OS_FREELIBRARY = 'Preemptive' + PREEMPTIVE_OS_GENERICOPS = 'Preemptive' + PREEMPTIVE_OS_GETADDRINFO = 'Preemptive' + PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE = 'Preemptive' + PREEMPTIVE_OS_GETDISKFREESPACE = 'Preemptive' + PREEMPTIVE_OS_GETFILEATTRIBUTES = 'Preemptive' + PREEMPTIVE_OS_GETFILESIZE = 'Preemptive' + PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE = 'Preemptive' + PREEMPTIVE_OS_GETLONGPATHNAME = 'Preemptive' + PREEMPTIVE_OS_GETPROCADDRESS = 'Preemptive' + PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT = 'Preemptive' + PREEMPTIVE_OS_GETVOLUMEPATHNAME = 'Preemptive' + PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_LIBRARYOPS = 'Preemptive' + PREEMPTIVE_OS_LOADLIBRARY = 'Preemptive' + PREEMPTIVE_OS_LOGONUSER = 'Preemptive' + PREEMPTIVE_OS_LOOKUPACCOUNTSID = 'Preemptive' + PREEMPTIVE_OS_MESSAGEQUEUEOPS = 'Preemptive' + PREEMPTIVE_OS_MOVEFILE = 'Preemptive' + PREEMPTIVE_OS_NETGROUPGETUSERS = 'Preemptive' + PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS = 'Preemptive' + PREEMPTIVE_OS_NETUSERGETGROUPS = 'Preemptive' + PREEMPTIVE_OS_NETUSERGETLOCALGROUPS = 'Preemptive' + PREEMPTIVE_OS_NETUSERMODALSGET = 'Preemptive' + PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY = 'Preemptive' + PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE = 'Preemptive' + PREEMPTIVE_OS_OPENDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_PDH_WMI_INIT = 'Preemptive' + PREEMPTIVE_OS_PIPEOPS = 'Preemptive' + PREEMPTIVE_OS_PROCESSOPS = 'Preemptive' + PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES = 'Preemptive' + PREEMPTIVE_OS_QUERYREGISTRY = 'Preemptive' + PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN = 'Preemptive' + PREEMPTIVE_OS_REMOVEDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_REPORTEVENT = 'Preemptive' + PREEMPTIVE_OS_REVERTTOSELF = 'Preemptive' + PREEMPTIVE_OS_RSFXDEVICEOPS = 'Preemptive' + PREEMPTIVE_OS_SECURITYOPS = 'Preemptive' + PREEMPTIVE_OS_SERVICEOPS = 'Preemptive' + PREEMPTIVE_OS_SETENDOFFILE = 'Preemptive' + PREEMPTIVE_OS_SETFILEPOINTER = 'Preemptive' + PREEMPTIVE_OS_SETFILEVALIDDATA = 'Preemptive' + PREEMPTIVE_OS_SETNAMEDSECURITYINFO = 'Preemptive' + PREEMPTIVE_OS_SQLCLROPS = 'Preemptive' + PREEMPTIVE_OS_SQMLAUNCH = 'Preemptive' + PREEMPTIVE_OS_VERIFYSIGNATURE = 'Preemptive' + PREEMPTIVE_OS_VERIFYTRUST = 'Preemptive' + PREEMPTIVE_OS_VSSOPS = 'Preemptive' + PREEMPTIVE_OS_WAITFORSINGLEOBJECT = 'Preemptive' + PREEMPTIVE_OS_WINSOCKOPS = 'Preemptive' + PREEMPTIVE_OS_WRITEFILE = 'Preemptive' + PREEMPTIVE_OS_WRITEFILEGATHER = 'Preemptive' + PREEMPTIVE_OS_WSASETLASTERROR = 'Preemptive' + PREEMPTIVE_REENLIST = 'Preemptive' + PREEMPTIVE_RESIZELOG = 'Preemptive' + PREEMPTIVE_ROLLFORWARDREDO = 'Preemptive' + PREEMPTIVE_ROLLFORWARDUNDO = 'Preemptive' + PREEMPTIVE_SB_STOPENDPOINT = 'Preemptive' + PREEMPTIVE_SERVER_STARTUP = 'Preemptive' + PREEMPTIVE_SETRMINFO = 'Preemptive' + PREEMPTIVE_SHAREDMEM_GETDATA = 'Preemptive' + PREEMPTIVE_SNIOPEN = 'Preemptive' + PREEMPTIVE_SOSHOST = 'Preemptive' + PREEMPTIVE_SOSTESTING = 'Preemptive' + PREEMPTIVE_SP_SERVER_DIAGNOSTICS = 'Preemptive' + PREEMPTIVE_STARTRM = 'Preemptive' + PREEMPTIVE_STREAMFCB_CHECKPOINT = 'Preemptive' + PREEMPTIVE_STREAMFCB_RECOVER = 'Preemptive' + PREEMPTIVE_STRESSDRIVER = 'Preemptive' + PREEMPTIVE_TESTING = 'Preemptive' + PREEMPTIVE_TRANSIMPORT = 'Preemptive' + PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN = 'Preemptive' + PREEMPTIVE_VSS_CREATESNAPSHOT = 'Preemptive' + PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT = 'Preemptive' + PREEMPTIVE_XE_CALLBACKEXECUTE = 'Preemptive' + PREEMPTIVE_XE_CX_FILE_OPEN = 'Preemptive' + PREEMPTIVE_XE_CX_HTTP_CALL = 'Preemptive' + PREEMPTIVE_XE_DISPATCHER = 'Preemptive' + PREEMPTIVE_XE_ENGINEINIT = 'Preemptive' + PREEMPTIVE_XE_GETTARGETSTATE = 'Preemptive' + PREEMPTIVE_XE_SESSIONCOMMIT = 'Preemptive' + PREEMPTIVE_XE_TARGETFINALIZE = 'Preemptive' + PREEMPTIVE_XE_TARGETINIT = 'Preemptive' + PREEMPTIVE_XE_TIMERRUN = 'Preemptive' + PREEMPTIVE_XETESTING = 'Preemptive' + PWAIT_HADR_ACTION_COMPLETED = 'Replication' + PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC = 'Replication' + PWAIT_HADR_CLUSTER_INTEGRATION = 'Replication' + PWAIT_HADR_FAILOVER_COMPLETED = 'Replication' + PWAIT_HADR_JOIN = 'Replication' + PWAIT_HADR_OFFLINE_COMPLETED = 'Replication' + PWAIT_HADR_ONLINE_COMPLETED = 'Replication' + PWAIT_HADR_POST_ONLINE_COMPLETED = 'Replication' + PWAIT_HADR_SERVER_READY_CONNECTIONS = 'Replication' + PWAIT_HADR_WORKITEM_COMPLETED = 'Replication' + PWAIT_HADRSIM = 'Replication' + PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC = 'Full Text Search' + QUERY_TRACEOUT = 'Tracing' + REPL_CACHE_ACCESS = 'Replication' + REPL_HISTORYCACHE_ACCESS = 'Replication' + REPL_SCHEMA_ACCESS = 'Replication' + REPL_TRANFSINFO_ACCESS = 'Replication' + REPL_TRANHASHTABLE_ACCESS = 'Replication' + REPL_TRANTEXTINFO_ACCESS = 'Replication' + REPLICA_WRITES = 'Replication' + REQUEST_FOR_DEADLOCK_SEARCH = 'Idle' + RESERVED_MEMORY_ALLOCATION_EXT = 'Memory' + RESOURCE_SEMAPHORE = 'Memory' + RESOURCE_SEMAPHORE_QUERY_COMPILE = 'Compilation' + SLEEP_BPOOL_FLUSH = 'Idle' + SLEEP_BUFFERPOOL_HELPLW = 'Idle' + SLEEP_DBSTARTUP = 'Idle' + SLEEP_DCOMSTARTUP = 'Idle' + SLEEP_MASTERDBREADY = 'Idle' + SLEEP_MASTERMDREADY = 'Idle' + SLEEP_MASTERUPGRADED = 'Idle' + SLEEP_MEMORYPOOL_ALLOCATEPAGES = 'Idle' + SLEEP_MSDBSTARTUP = 'Idle' + SLEEP_RETRY_VIRTUALALLOC = 'Idle' + SLEEP_SYSTEMTASK = 'Idle' + SLEEP_TASK = 'Idle' + SLEEP_TEMPDBSTARTUP = 'Idle' + SLEEP_WORKSPACE_ALLOCATEPAGE = 'Idle' + SOS_SCHEDULER_YIELD = 'CPU' + SQLCLR_APPDOMAIN = 'SQL CLR' + SQLCLR_ASSEMBLY = 'SQL CLR' + SQLCLR_DEADLOCK_DETECTION = 'SQL CLR' + SQLCLR_QUANTUM_PUNISHMENT = 'SQL CLR' + SQLTRACE_BUFFER_FLUSH = 'Idle' + SQLTRACE_FILE_BUFFER = 'Tracing' + SQLTRACE_FILE_READ_IO_COMPLETION = 'Tracing' + SQLTRACE_FILE_WRITE_IO_COMPLETION = 'Tracing' + SQLTRACE_INCREMENTAL_FLUSH_SLEEP = 'Idle' + SQLTRACE_PENDING_BUFFER_WRITERS = 'Tracing' + SQLTRACE_SHUTDOWN = 'Tracing' + SQLTRACE_WAIT_ENTRIES = 'Idle' + THREADPOOL = 'Worker Thread' + TRACE_EVTNOTIF = 'Tracing' + TRACEWRITE = 'Tracing' + TRAN_MARKLATCH_DT = 'Transaction' + TRAN_MARKLATCH_EX = 'Transaction' + TRAN_MARKLATCH_KP = 'Transaction' + TRAN_MARKLATCH_NL = 'Transaction' + TRAN_MARKLATCH_SH = 'Transaction' + TRAN_MARKLATCH_UP = 'Transaction' + TRANSACTION_MUTEX = 'Transaction' + WAIT_FOR_RESULTS = 'User Wait' + WAITFOR = 'User Wait' + WRITE_COMPLETION = 'Other Disk IO' + WRITELOG = 'Tran Log IO' + XACT_OWN_TRANSACTION = 'Transaction' + XACT_RECLAIM_SESSION = 'Transaction' + XACTLOCKINFO = 'Transaction' + XACTWORKSPACE_MUTEX = 'Transaction' + XE_DISPATCHER_WAIT = 'Idle' + XE_TIMER_EVENT = 'Idle' + ABR = 'Other' + ASSEMBLY_LOAD = 'SQLCLR' + ASYNC_DISKPOOL_LOCK = 'Buffer I/O' + BACKUP = 'Backup' + BACKUP_CLIENTLOCK = 'Backup' + BACKUP_OPERATOR = 'Backup' + BACKUPBUFFER = 'Backup' + BACKUPTHREAD = 'Backup' + BAD_PAGE_PROCESS = 'Other' + BUILTIN_HASHKEY_MUTEX = 'Other' + CHECK_PRINT_RECORD = 'Other' + CPU = 'CPU' + CURSOR = 'Other' + CURSOR_ASYNC = 'Other' + DAC_INIT = 'Other' + DBCC_COLUMN_TRANSLATION_CACHE = 'Other' + DBTABLE = 'Other' + DEADLOCK_ENUM_MUTEX = 'Latch' + DEADLOCK_TASK_SEARCH = 'Other' + DEBUG = 'Other' + DISABLE_VERSIONING = 'Other' + DISKIO_SUSPEND = 'Backup' + DLL_LOADING_MUTEX = 'Other' + DROPTEMP = 'Other' + DUMP_LOG_COORDINATOR = 'Other' + DUMP_LOG_COORDINATOR_QUEUE = 'Other' + DUMPTRIGGER = 'Other' + EC = 'Other' + EE_SPECPROC_MAP_INIT = 'Other' + ENABLE_VERSIONING = 'Other' + ERROR_REPORTING_MANAGER = 'Other' + EXECSYNC = 'Parallelism' + EXECUTION_PIPE_EVENT_INTERNAL = 'Other' + FAILPOINT = 'Other' + FS_GARBAGE_COLLECTOR_SHUTDOWN = 'SQLCLR' + FSAGENT = 'Idle' + FT_RESUME_CRAWL = 'Other' + GUARDIAN = 'Other' + HTTP_ENDPOINT_COLLCREATE = 'Other' + HTTP_ENUMERATION = 'Other' + HTTP_START = 'Other' + IMP_IMPORT_MUTEX = 'Other' + IMPPROV_IOWAIT = 'Other' + INDEX_USAGE_STATS_MUTEX = 'Latch' + INTERNAL_TESTING = 'Other' + IO_AUDIT_MUTEX = 'Other' + KSOURCE_WAKEUP = 'Idle' + KTM_ENLISTMENT = 'Other' + KTM_RECOVERY_MANAGER = 'Other' + KTM_RECOVERY_RESOLUTION = 'Other' + LOWFAIL_MEMMGR_QUEUE = 'Memory' + MIRROR_SEND_MESSAGE = 'Other' + MISCELLANEOUS = 'Other' + MSQL_DQ = 'Network I/O' + MSQL_SYNC_PIPE = 'Other' + MSQL_XP = 'Other' + OLEDB = 'Network I/O' + PARALLEL_BACKUP_QUEUE = 'Other' + PRINT_ROLLBACK_PROGRESS = 'Other' + QNMANAGER_ACQUIRE = 'Other' + QPJOB_KILL = 'Other' + QPJOB_WAITFOR_ABORT = 'Other' + QRY_MEM_GRANT_INFO_MUTEX = 'Other' + QUERY_ERRHDL_SERVICE_DONE = 'Other' + QUERY_EXECUTION_INDEX_SORT_EVENT_OPEN = 'Other' + QUERY_NOTIFICATION_MGR_MUTEX = 'Other' + QUERY_NOTIFICATION_SUBSCRIPTION_MUTEX = 'Other' + QUERY_NOTIFICATION_TABLE_MGR_MUTEX = 'Other' + QUERY_NOTIFICATION_UNITTEST_MUTEX = 'Other' + QUERY_OPTIMIZER_PRINT_MUTEX = 'Other' + QUERY_REMOTE_BRICKS_DONE = 'Other' + RECOVER_CHANGEDB = 'Other' + REQUEST_DISPENSER_PAUSE = 'Other' + RESOURCE_QUEUE = 'Idle' + RESOURCE_SEMAPHORE_MUTEX = 'Compilation' + RESOURCE_SEMAPHORE_SMALL_QUERY = 'Compilation' + SEC_DROP_TEMP_KEY = 'Other' + SEQUENTIAL_GUID = 'Other' + SERVER_IDLE_CHECK = 'Idle' + SHUTDOWN = 'Other' + SNI_CRITICAL_SECTION = 'Other' + SNI_HTTP_ACCEPT = 'Idle' + SNI_HTTP_WAITFOR_0_DISCON = 'Other' + SNI_LISTENER_ACCESS = 'Other' + SNI_TASK_COMPLETION = 'Other' + SOAP_READ = 'Full Text Search' + SOAP_WRITE = 'Full Text Search' + SOS_CALLBACK_REMOVAL = 'Other' + SOS_DISPATCHER_MUTEX = 'Other' + SOS_LOCALALLOCATORLIST = 'Other' + SOS_OBJECT_STORE_DESTROY_MUTEX = 'Other' + SOS_PROCESS_AFFINITY_MUTEX = 'Other' + SOS_RESERVEDMEMBLOCKLIST = 'Memory' + SOS_STACKSTORE_INIT_MUTEX = 'Other' + SOS_SYNC_TASK_ENQUEUE_EVENT = 'Other' + SOS_VIRTUALMEMORY_LOW = 'Memory' + SOSHOST_EVENT = 'Other' + SOSHOST_INTERNAL = 'Other' + SOSHOST_MUTEX = 'Other' + SOSHOST_RWLOCK = 'Other' + SOSHOST_SEMAPHORE = 'Other' + SOSHOST_SLEEP = 'Other' + SOSHOST_TRACELOCK = 'Other' + SOSHOST_WAITFORDONE = 'Other' + SQLSORT_NORMMUTEX = 'Other' + SQLSORT_SORTMUTEX = 'Other' + SQLTRACE_LOCK = 'Other' + SRVPROC_SHUTDOWN = 'Other' + TEMPOBJ = 'Other' + TIMEPRIV_TIMEPERIOD = 'Other' + UTIL_PAGE_ALLOC = 'Memory' + VIA_ACCEPT = 'Other' + VIEW_DEFINITION_MUTEX = 'Latch' + WAITFOR_TASKSHUTDOWN = 'Idle' + WAITSTAT_MUTEX = 'Other' + WCC = 'Other' + WORKTBL_DROP = 'Other' + XE_BUFFERMGR_ALLPROCECESSED_EVENT = 'Other' + XE_BUFFERMGR_FREEBUF_EVENT = 'Other' + XE_DISPATCHER_JOIN = 'Other' + XE_MODULEMGR_SYNC = 'Other' + XE_OLS_LOCK = 'Other' + XE_SERVICES_MUTEX = 'Other' + XE_SESSION_CREATE_SYNC = 'Other' + XE_SESSION_SYNC = 'Other' + XE_STM_CREATE = 'Other' + XE_TIMER_MUTEX = 'Other' + XE_TIMER_TASK_DONE = 'Other' + } + + $ignorable = 'BROKER_EVENTHANDLER', 'BROKER_RECEIVE_WAITFOR', 'BROKER_TASK_STOP', + 'BROKER_TO_FLUSH', 'BROKER_TRANSMITTER', 'CHECKPOINT_QUEUE', + 'CHKPT', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'CLR_SEMAPHORE', + 'DBMIRROR_DBM_EVENT', 'DBMIRROR_EVENTS_QUEUE', 'DBMIRROR_WORKER_QUEUE', + 'DBMIRRORING_CMD', 'DIRTY_PAGE_POLL', 'DISPATCHER_QUEUE_SEMAPHORE', + 'EXECSYNC', 'FSAGENT', 'FT_IFTS_SCHEDULER_IDLE_WAIT', 'FT_IFTSHC_MUTEX', + 'HADR_CLUSAPI_CALL', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'HADR_LOGCAPTURE_WAIT', + 'HADR_NOTIFICATION_DEQUEUE', 'HADR_TIMER_TASK', 'HADR_WORK_QUEUE', + 'KSOURCE_WAKEUP', 'LAZYWRITER_SLEEP', 'LOGMGR_QUEUE', + 'MEMORY_ALLOCATION_EXT', 'ONDEMAND_TASK_QUEUE', 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS', + 'PARALLEL_REDO_WORKER_WAIT_WORK', 'PREEMPTIVE_HADR_LEASE_MECHANISM', + 'PREEMPTIVE_OS_LIBRARYOPS', 'PREEMPTIVE_OS_COMOPS', 'PREEMPTIVE_OS_CRYPTOPS', + 'PREEMPTIVE_OS_PIPEOPS', 'PREEMPTIVE_OS_AUTHENTICATIONOPS', + 'PREEMPTIVE_OS_GENERICOPS', 'PREEMPTIVE_OS_VERIFYTRUST', + 'PREEMPTIVE_OS_FILEOPS', 'PREEMPTIVE_OS_DEVICEOPS', 'PREEMPTIVE_OS_QUERYREGISTRY', + 'PREEMPTIVE_OS_WRITEFILE', 'PREEMPTIVE_XE_CALLBACKEXECUTE', 'PREEMPTIVE_XE_DISPATCHER', + 'PREEMPTIVE_XE_GETTARGETSTATE', 'PREEMPTIVE_XE_SESSIONCOMMIT', + 'PREEMPTIVE_XE_TARGETINIT', 'PREEMPTIVE_XE_TARGETFINALIZE', + 'PWAIT_ALL_COMPONENTS_INITIALIZED', 'PWAIT_DIRECTLOGCONSUMER_GETNEXT', + 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'QDS_ASYNC_QUEUE', + 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', + 'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_BPOOL_FLUSH', 'SLEEP_DBSTARTUP', + 'SLEEP_DCOMSTARTUP', 'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', + 'SLEEP_MASTERUPGRADED', 'SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK', 'SLEEP_TASK', + 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT', 'SP_SERVER_DIAGNOSTICS_SLEEP', + 'SQLTRACE_BUFFER_FLUSH', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'SQLTRACE_WAIT_ENTRIES', + 'WAIT_FOR_RESULTS', 'WAITFOR', 'WAITFOR_TASKSHUTDOWN', 'WAIT_XTP_HOST_WAIT', + 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'WAIT_XTP_CKPT_CLOSE', 'WAIT_XTP_RECOVERY', + 'XE_BUFFERMGR_ALLPROCESSED_EVENT', 'XE_DISPATCHER_JOIN', + 'XE_DISPATCHER_WAIT', 'XE_LIVE_TARGET_TVF', 'XE_TIMER_EVENT' + + if ($IncludeIgnorable) { + $sql = "WITH [Waits] AS + (SELECT + [wait_type], + [wait_time_ms] / 1000.0 AS [WaitS], + ([wait_time_ms] - [signal_wait_time_ms]) / 1000.0 AS [ResourceS], + [signal_wait_time_ms] / 1000.0 AS [SignalS], + [waiting_tasks_count] AS [WaitCount], + 100.0 * [wait_time_ms] / SUM ([wait_time_ms]) OVER() AS [Percentage], + ROW_NUMBER() OVER(ORDER BY [wait_time_ms] DESC) AS [RowNum] + FROM sys.dm_os_wait_stats + WHERE [waiting_tasks_count] > 0 + ) + SELECT + MAX ([W1].[wait_type]) AS [WaitType], + CAST (MAX ([W1].[WaitS]) AS DECIMAL (16,2)) AS [WaitSeconds], + CAST (MAX ([W1].[ResourceS]) AS DECIMAL (16,2)) AS [ResourceSeconds], + CAST (MAX ([W1].[SignalS]) AS DECIMAL (16,2)) AS [SignalSeconds], + MAX ([W1].[WaitCount]) AS [WaitCount], + CAST (MAX ([W1].[Percentage]) AS DECIMAL (5,2)) AS [Percentage], + CAST ((MAX ([W1].[WaitS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgWaitSeconds], + CAST ((MAX ([W1].[ResourceS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgResSeconds], + CAST ((MAX ([W1].[SignalS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgSigSeconds], + CAST ('https://www.sqlskills.com/help/waits/' + MAX ([W1].[wait_type]) as XML) AS [URL] + FROM [Waits] AS [W1] + INNER JOIN [Waits] AS [W2] + ON [W2].[RowNum] <= [W1].[RowNum] + GROUP BY [W1].[RowNum] HAVING SUM ([W2].[Percentage]) - MAX([W1].[Percentage]) < $Threshold" + } + else { + $IgnorableList = "'$($ignorable -join "','")'" + $sql = "WITH [Waits] AS + (SELECT + [wait_type], + [wait_time_ms] / 1000.0 AS [WaitS], + ([wait_time_ms] - [signal_wait_time_ms]) / 1000.0 AS [ResourceS], + [signal_wait_time_ms] / 1000.0 AS [SignalS], + [waiting_tasks_count] AS [WaitCount], + 100.0 * [wait_time_ms] / SUM ([wait_time_ms]) OVER() AS [Percentage], + ROW_NUMBER() OVER(ORDER BY [wait_time_ms] DESC) AS [RowNum] + FROM sys.dm_os_wait_stats + WHERE [waiting_tasks_count] > 0 + AND Cast([wait_type] as VARCHAR(60)) NOT IN ($IgnorableList) + ) + SELECT + MAX ([W1].[wait_type]) AS [WaitType], + CAST (MAX ([W1].[WaitS]) AS DECIMAL (16,2)) AS [WaitSeconds], + CAST (MAX ([W1].[ResourceS]) AS DECIMAL (16,2)) AS [ResourceSeconds], + CAST (MAX ([W1].[SignalS]) AS DECIMAL (16,2)) AS [SignalSeconds], + MAX ([W1].[WaitCount]) AS [WaitCount], + CAST (MAX ([W1].[Percentage]) AS DECIMAL (5,2)) AS [Percentage], + CAST ((MAX ([W1].[WaitS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgWaitSeconds], + CAST ((MAX ([W1].[ResourceS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgResSeconds], + CAST ((MAX ([W1].[SignalS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgSigSeconds], + CAST ('https://www.sqlskills.com/help/waits/' + MAX ([W1].[wait_type]) as XML) AS [URL] + FROM [Waits] AS [W1] + INNER JOIN [Waits] AS [W2] + ON [W2].[RowNum] <= [W1].[RowNum] + GROUP BY [W1].[RowNum] HAVING SUM ([W2].[Percentage]) - MAX([W1].[Percentage]) < $Threshold" + + } + Write-Message -Level Debug -Message $sql + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + Write-Message -Level Verbose -Message "Connected to $instance" + if ($IncludeIgnorable) { + $excludeColumns = 'Notes' + } + else { + $excludeColumns = 'Notes', 'Ignorable' + } + + foreach ($row in $server.Query($sql)) { + $waitType = $row.WaitType + if (-not $IncludeIgnorable) { + if ($ignorable -contains $waitType) { continue } + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + WaitType = $waitType + Category = ($category).$waitType + WaitSeconds = $row.WaitSeconds + ResourceSeconds = $row.ResourceSeconds + SignalSeconds = $row.SignalSeconds + WaitCount = $row.WaitCount + Percentage = $row.Percentage + AverageWaitSeconds = $row.AvgWaitSeconds + AverageResourceSeconds = $row.AvgResSeconds + AverageSignalSeconds = $row.AvgSigSeconds + Ignorable = ($ignorable -contains $waitType) + URL = $row.URL + Notes = ($details).$waitType + } | Select-DefaultView -ExcludeProperty $excludeColumns + } + } + } +} +function Get-DbaWindowsLog { + <# + .SYNOPSIS + Gets Windows Application events associated with an instance + + .DESCRIPTION + Gets Windows Application events associated with an instance + + .PARAMETER SqlInstance + The instance(s) to retrieve the event logs from + + .PARAMETER Start + Default: 1970 + Retrieve all events starting from this timestamp. + + .PARAMETER End + Default: Now + Retrieve all events that happened before this timestamp + + .PARAMETER Credential + Credential to be used to connect to the Server. Note this is a Windows credential, as this command requires we communicate with the computer and not with the SQL instance. + + .PARAMETER MaxThreads + Default: Unlimited + The maximum number of parallel threads used on the local computer. + Given that those will mostly be waiting for the remote system, there is usually no need to limit this. + + .PARAMETER MaxRemoteThreads + Default: 2 + The maximum number of parallel threads that are executed on the target sql server. + These processes will cause considerable CPU load, so a low limit is advisable in most scenarios. + Any value lower than 1 disables the limit + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Logging + Author: Drew Furgiuele + Editor: Friedrich "Fred" Weinmann + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWindowsLog + + .EXAMPLE + $ErrorLogs = Get-DbaWindowsLog -SqlInstance sql01\sharepoint + $ErrorLogs | Where-Object ErrorNumber -eq 18456 + + Returns all lines in the errorlogs that have event number 18456 in them + +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [DateTime] + $Start = "1/1/1970 00:00:00", + + [DateTime] + $End = (Get-Date), + + + [System.Management.Automation.PSCredential] + $Credential, + + [int] + $MaxThreads = 0, + + [int] + $MaxRemoteThreads = 2, + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + Write-Message -Level Debug -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + #region Helper Functions + function Start-Runspace { + $Powershell = [PowerShell]::Create().AddScript($scriptBlock_ParallelRemoting).AddParameter("SqlInstance", $instance).AddParameter("Start", $Start).AddParameter("End", $End).AddParameter("Credential", $Credential).AddParameter("MaxRemoteThreads", $MaxRemoteThreads).AddParameter("ScriptBlock", $scriptBlock_RemoteExecution) + $Powershell.RunspacePool = $RunspacePool + Write-Message -Level Verbose -Message "Launching remote runspace against $instance" -Target $instance + $null = $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{ Runspace = $PowerShell.BeginInvoke(); PowerShell = $PowerShell; Instance = $instance.FullSmoName })) + } + + function Receive-Runspace { + [Parameter()] + Param ( + [switch] + $Wait + ) + + do { + foreach ($Run in $RunspaceCollection.ToArray()) { + if ($Run.Runspace.IsCompleted) { + Write-Message -Level Verbose -Message "Receiving results from $($Run.Instance)" -Target $Run.Instance + $Run.PowerShell.EndInvoke($Run.Runspace) + $Run.PowerShell.Dispose() + $RunspaceCollection.Remove($Run) + } + } + + if ($Wait -and ($RunspaceCollection.Count -gt 0)) { Start-Sleep -Milliseconds 250 } + } + while ($Wait -and ($RunspaceCollection.Count -gt 0)) + } + #endregion Helper Functions + + #region Scriptblocks + $scriptBlock_RemoteExecution = { + Param ( + [System.DateTime] + $Start, + + [System.DateTime] + $End, + + [string] + $InstanceName, + + [int] + $Throttle + ) + + #region Helper function + function Convert-ErrorRecord { + Param ( + $Line + ) + + if (Get-Variable -Name codesAndStuff -Scope 1) { + $line2 = (Get-Variable -Name codesAndStuff -Scope 1).Value + Remove-Variable -Name codesAndStuff -Scope 1 + + $groups = [regex]::Matches($line2, '^([\d- :]+.\d\d) (\w+)[ ]+Error: (\d+), Severity: (\d+), State: (\d+)').Groups + $groups2 = [regex]::Matches($line, '^[\d- :]+.\d\d \w+[ ]+(.*)$').Groups + + New-Object PSObject -Property @{ + Timestamp = [DateTime]::ParseExact($groups[1].Value, "yyyy-MM-dd HH:mm:ss.ff", $null) + Spid = $groups[2].Value + Message = $groups2[1].Value + ErrorNumber = [int]($groups[3].Value) + Severity = [int]($groups[4].Value) + State = [int]($groups[5].Value) + } + } + + if ($Line -match '^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d[\w ]+((\w+): (\d+)[,\.]\s?){3}') { + Set-Variable -Name codesAndStuff -Value $Line -Scope 1 + } + } + #endregion Helper function + + #region Script that processes an individual file + $scriptBlock = { + Param ( + [System.IO.FileInfo] + $File + ) + + try { + $stream = New-Object System.IO.FileStream($File.FullName, "Open", "Read", "ReadWrite, Delete") + $reader = New-Object System.IO.StreamReader($stream) + + while (-not $reader.EndOfStream) { + Convert-ErrorRecord -Line $reader.ReadLine() + } + } + catch { } + } + #endregion Script that processes an individual file + + #region Gather list of files to process + $eventSource = "MSSQLSERVER" + if ($InstanceName -notmatch "^DEFAULT$|^MSSQLSERVER$") { + $eventSource = 'MSSQL$' + $InstanceName + } + + $event = Get-WinEvent -FilterHashtable @{ + LogName = "Application" + ID = 17111 + ProviderName = $eventSource + } -MaxEvents 1 -ErrorAction SilentlyContinue + + if (-not $event) { return } + + $path = $event.Properties[0].Value + $errorLogPath = Split-Path -Path $path + $errorLogFileName = Split-Path -Path $path -Leaf + $errorLogFiles = Get-ChildItem -Path $errorLogPath | Where-Object { ($_.Name -like "$errorLogFileName*") -and ($_.LastWriteTime -gt $Start) -and ($_.CreationTime -lt $End) } + #endregion Gather list of files to process + + #region Prepare Runspaces + [Collections.Arraylist]$RunspaceCollection = @() + + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $Command = Get-Item function:Convert-ErrorRecord + $InitialSessionState.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($command.Name, $command.Definition))) + + $RunspacePool = [RunspaceFactory]::CreateRunspacePool($InitialSessionState) + $null = $RunspacePool.SetMinRunspaces(1) + if ($Throttle -gt 0) { $null = $RunspacePool.SetMaxRunspaces($Throttle) } + $RunspacePool.Open() + #endregion Prepare Runspaces + + #region Process Error files + $countDone = 0 + $countStarted = 0 + $countTotal = ($errorLogFiles | Measure-Object).Count + + while ($countDone -lt $countTotal) { + while (($RunspacePool.GetAvailableRunspaces() -gt 0) -and ($countStarted -lt $countTotal)) { + $Powershell = [PowerShell]::Create().AddScript($scriptBlock).AddParameter("File", $errorLogFiles[$countStarted]) + $Powershell.RunspacePool = $RunspacePool + $null = $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{ Runspace = $PowerShell.BeginInvoke(); PowerShell = $PowerShell })) + $countStarted++ + } + + foreach ($Run in $RunspaceCollection.ToArray()) { + if ($Run.Runspace.IsCompleted) { + $Run.PowerShell.EndInvoke($Run.Runspace) | Where-Object { ($_.Timestamp -gt $Start) -and ($_.Timestamp -lt $End) } + $Run.PowerShell.Dispose() + $RunspaceCollection.Remove($Run) + $countDone++ + } + } + + Start-Sleep -Milliseconds 250 + } + $RunspacePool.Close() + $RunspacePool.Dispose() + #endregion Process Error files + } + + $scriptBlock_ParallelRemoting = { + Param ( + [DbaInstanceParameter] + $SqlInstance, + + [DateTime] + $Start, + + [DateTime] + $End, + + [object] + $Credential, + + [int] + $MaxRemoteThreads, + + [System.Management.Automation.ScriptBlock] + $ScriptBlock + ) + + $params = @{ + ArgumentList = $Start, $End, $SqlInstance.InstanceName, $MaxRemoteThreads + ScriptBlock = $ScriptBlock + } + if (-not $SqlInstance.IsLocalhost) { $params["ComputerName"] = $SqlInstance.ComputerName } + if ($Credential) { $params["Credential"] = $Credential } + + Invoke-Command @params | Select-Object @{ n = "InstanceName"; e = { $SqlInstance.FullSmoName } }, Timestamp, Spid, Severity, ErrorNumber, State, Message + } + #endregion Scriptblocks + + #region Setup Runspace + [Collections.Arraylist]$RunspaceCollection = @() + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $RunspacePool = [RunspaceFactory]::CreateRunspacePool($InitialSessionState) + $RunspacePool.SetMinRunspaces(1) | Out-Null + if ($MaxThreads -gt 0) { $null = $RunspacePool.SetMaxRunspaces($MaxThreads) } + $RunspacePool.Open() + + $countStarted = 0 + $countReceived = 0 + #endregion Setup Runspace + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + Start-Runspace + Receive-Runspace + } + } + + end { + Receive-Runspace -Wait + $RunspacePool.Close() + $RunspacePool.Dispose() + } +} +function Get-DbaXEObject { + <# + .SYNOPSIS + Gets a list of trace(s) from specified SQL Server instance(s). + + .DESCRIPTION + This function returns a list of Traces on the specified SQL Server instance(s) and identifies the default Trace File + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Type + Used to specify the type. Valid types include: + + Action + Event + Map + Message + PredicateComparator + PredicateSource + Target + Type + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaXEObject -SqlInstance sql2016 + + Lists all the XE Objects on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaXEObject -SqlInstance sql2017 -Type Action, Event + + Lists all the XE Objects of type Action and Event on the sql2017 SQL Server. + + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [ValidateSet("Type", "Event", "Target", "Action", "Map", "Message", "PredicateComparator", "PredicateSource")] + [string[]]$Type, + [switch]$EnableException + ) + begin { + if ($Type) { + $join = $Type -join "','" + $where = "AND o.object_type in ('$join')" + $where.Replace("PredicateComparator", "pred_compare") + $where.Replace("PredicateSource", "pred_source") + } + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + p.name AS PackageName, + ObjectType = + CASE o.object_type + WHEN 'type' THEN 'Type' + WHEN 'event' THEN 'Event' + WHEN 'target' THEN 'Target' + WHEN 'pred_compare' THEN 'PredicateComparator' + WHEN 'pred_source' THEN 'PredicateSource' + WHEN 'action' THEN 'Action' + WHEN 'map' THEN 'Map' + WHEN 'message' THEN 'Message' + ELSE o.object_type + END, + o.object_type as ObjectTypeRaw, + o.name AS TargetName, + o.description as Description + FROM sys.dm_xe_packages AS p + JOIN sys.dm_xe_objects AS o ON p.guid = o.package_guid + WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0) + $where + AND (o.capabilities IS NULL OR o.capabilities & 1 = 0) + ORDER BY o.object_type + " + } + process { + foreach ($instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + try { + $server.Query($sql) | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, ObjectTypeRaw + } + catch { + Stop-Function -Message "Issue collecting trace data on $server." -Target $server -ErrorRecord $_ + } + } + } +} +function Get-DbaXESession { + <# + .SYNOPSIS + Gets a list of Extended Events Sessions from the specified SQL Server instance(s). + + .DESCRIPTION + Retrieves a list of Extended Events Sessions present on the specified SQL Server instance(s). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Only return specific sessions. Options for this parameter are auto-populated from the server. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESession + + .EXAMPLE + Get-DbaXESession -SqlInstance ServerA\sql987 + + Returns a custom object with ComputerName, SQLInstance, Session, StartTime, Status and other properties. + + .EXAMPLE + Get-DbaXESession -SqlInstance ServerA\sql987 | Format-Table ComputerName, SqlInstance, Session, Status -AutoSize + + Returns a formatted table displaying ComputerName, SqlInstance, Session, and Status. + + .EXAMPLE + 'ServerA\sql987','ServerB' | Get-DbaXESession + + Returns a custom object with ComputerName, SqlInstance, Session, StartTime, Status and other properties, from multiple SQL instances. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Sessions")] + [object[]]$Session, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-DbaXEsSession + } + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 -AzureUnsupported + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $XEStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + Write-Message -Level Verbose -Message "Getting XEvents Sessions on $instance." + + $xesessions = $XEStore.sessions + + if ($Session) { + $xesessions = $xesessions | Where-Object { $_.Name -in $Session } + } + + foreach ($x in $xesessions) { + $status = switch ($x.IsRunning) { $true { "Running" } $false { "Stopped" } } + $files = $x.Targets.TargetFields | Where-Object Name -eq Filename | Select-Object -ExpandProperty Value + + $filecollection = $remotefile = @() + + if ($files) { + foreach ($file in $files) { + if ($file -notmatch ':\\' -and $file -notmatch '\\\\') { + $directory = $server.ErrorLogPath.TrimEnd("\") + $file = "$directory\$file" + } + $filecollection += $file + $remotefile += Join-AdminUnc -servername $server.netName -filepath $file + } + } + + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Status -Value $status + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Session -Value $x.Name + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name TargetFile -Value $filecollection + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name RemoteTargetFile -Value $remotefile + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Parent -Value $server + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Store -Value $XEStore + Select-DefaultView -InputObject $x -Property ComputerName, InstanceName, SqlInstance, Name, Status, StartTime, AutoStart, State, Targets, TargetFile, Events, MaxMemory, MaxEventSize + } + } + } +} +function Get-DbaXESessionTarget { + <# + .SYNOPSIS + Get a list of Extended Events Session Targets from the specified SQL Server instance(s). + + .DESCRIPTION + Retrieves a list of Extended Events Session Targets from the specified SQL Server instance(s). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Only return a specific session. Options for this parameter are auto-populated from the server. + + .PARAMETER Target + Only return a specific target. + + .PARAMETER InputObject + Specifies an XE session returned by Get-DbaXESession to search. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTarget + + .EXAMPLE + Get-DbaXESessionTarget -SqlInstance ServerA\sql987 -Session system_health + + Shows targets for the system_health session on ServerA\sql987. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session system_health | Get-DbaXESessionTarget + + Returns the targets for the system_health session on sql2016. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session system_health | Get-DbaXESessionTarget -Target package0.event_file + + Return only the package0.event_file target for the system_health session on sql2016. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(ValueFromPipeline, ParameterSetName = "instance", Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Session, + [string[]]$Target, + [parameter(ValueFromPipeline, ParameterSetName = "piped", Mandatory)] + [Microsoft.SqlServer.Management.XEvent.Session[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + function Get-Target { + [CmdletBinding()] + param ( + $Sessions, + $Session, + $Server, + $Target + ) + + foreach ($xsession in $Sessions) { + + if ($null -eq $server) { + $server = $xsession.Parent + } + + if ($Session -and $xsession.Name -notin $Session) { continue } + $status = switch ($xsession.IsRunning) { $true { "Running" } $false { "Stopped" } } + $sessionname = $xsession.Name + + foreach ($xtarget in $xsession.Targets) { + if ($Target -and $xtarget.Name -notin $Target) { continue } + + $files = $xtarget.TargetFields | Where-Object Name -eq Filename | Select-Object -ExpandProperty Value + + $filecollection = $remotefile = @() + + if ($files) { + foreach ($file in $files) { + if ($file -notmatch ':\\' -and $file -notmatch '\\\\') { + $directory = $server.ErrorLogPath.TrimEnd("\") + $file = "$directory\$file" + } + $filecollection += $file + $remotefile += Join-AdminUnc -servername $server.netName -filepath $file + } + } + + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name Session -Value $sessionname + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name SessionStatus -Value $status + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name TargetFile -Value $filecollection + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name RemoteTargetFile -Value $remotefile + + Select-DefaultView -InputObject $xtarget -Property ComputerName, InstanceName, SqlInstance, Session, SessionStatus, Name, ID, 'TargetFields as Field', PackageName, 'TargetFile as File', Description, ScriptName + } + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaXESession -SqlInstance $instance -SqlCredential $SqlCredential -Session $Session + } + Get-Target -Sessions $InputObject -Session $Session -Target $Target + } +} +function Get-DbaXESessionTargetFile { + <# + .SYNOPSIS + Get a file system object from the Extended Events Session Target Files. + + .DESCRIPTION + Get a file system object from the Extended Events Session Target Files. + + Note: this performs a Get-ChildItem on remote servers if the specified target SQL Server is remote. + + .PARAMETER SqlInstance + The target SQL Server + + .PARAMETER SqlCredential + Login to SQL instnace with alternative credentials + + .PARAMETER Session + Only return files from a specific session. Options for this parameter are auto-populated from the server. + + .PARAMETER Target + Only return files from a specific target. + + .PARAMETER InputObject + Allows results from piping in Get-DbaXESessionTarget. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTargetFile + + .EXAMPLE + Get-DbaXESessionTargetFile -SqlInstance sql2017 -Session 'Long Running Queries' + + Shows Target Files for the 'Long Running Queries' session on sql2017. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session 'Long Running Queries' | Get-DbaXESessionTarget | Get-DbaXESessionTargetFile + + Returns the Target Files for the system_health session on sql2016. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(ValueFromPipeline, ParameterSetName = "instance", Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Session, + [string[]]$Target, + [parameter(ValueFromPipeline, ParameterSetName = "piped", Mandatory)] + [Microsoft.SqlServer.Management.XEvent.Target[]]$InputObject, + [switch]$EnableException + ) + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaXESessionTarget -SqlInstance $instance -SqlCredential $SqlCredential -Session $Session -Target $Target | Where-Object File -ne $null + } + + foreach ($object in $InputObject) { + $computer = [dbainstance]$object.ComputerName + try { + if ($computer.IsLocal) { + $file = $object.TargetFile + Write-Message -Level Verbose -Message "Getting $file" + Get-ChildItem "$file*" -ErrorAction Stop + } + else { + $file = $object.RemoteTargetFile + Write-Message -Level Verbose -Message "Getting $file" + Get-ChildItem -Recurse "$file*" -ErrorAction Stop + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } + } +} +function Get-DbaXESessionTemplate { + <# + .SYNOPSIS + Parses Extended Event XML templates. Defaults to parsing templates in the dbatools template repository (\bin\xetemplates\). + + .DESCRIPTION + Parses Extended Event XML templates. Defaults to parsing templates in the dbatools template repository (\bin\xetemplates\). + + The default repository contains templates from: + Microsoft's Templates that come with SSMS + Jes Borland's "Everyday Extended Events" presentation and GitHub repository (https://github.com/grrlgeek/extended-events) + Christian Gräfe's XE Repo: https://github.com/chrgraefe/sqlscripts/blob/master/XE-Events/ + Erin Stellato's Blog: https://www.sqlskills.com/blogs/erin/ + + Some profile templates converted using: + sp_SQLskills_ConvertTraceToExtendedEvents.sql + Jonathan M. Kehayias, SQLskills.com + http://sqlskills.com/blogs/jonathan + + .PARAMETER Path + The path to the template directory. Defaults to the dbatools template repository (\bin\xetemplates\). + + .PARAMETER Pattern + Specify a pattern for filtering. Alternatively, you can use Out-GridView -Passthru to select objects and pipe them to Import-DbaXESessionTemplate + + .PARAMETER Template + Specifies one or more of the templates provided by dbatools. Press tab to cycle through the list of options. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTemplate + + .EXAMPLE + Get-DbaXESessionTemplate + + Returns information about all the templates in the local dbatools repository. + + .EXAMPLE + Get-DbaXESessionTemplate | Out-GridView -PassThru | Import-DbaXESessionTemplate -SqlInstance sql2017 | Start-DbaXESession + + Allows you to select a Session template, then import it to the specified instance and start the session. + + .EXAMPLE + Get-DbaXESessionTemplate -Path "$home\Documents\SQL Server Management Studio\Templates\XEventTemplates" + + Returns information about all the templates in your local XEventTemplates repository. + + .EXAMPLE + Get-DbaXESessionTemplate -Pattern duration + + Returns information about all the templates that match the word "duration" in the title, category or body. + + .EXAMPLE + Get-DbaXESessionTemplate | Select-Object * + + Returns more information about the template, including the full path/filename. + #> + + [CmdletBinding()] + param ( + [string[]]$Path = "$script:PSModuleRoot\bin\xetemplates", + [string]$Pattern, + [string[]]$Template, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\xetemplates-metadata.xml" + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($directory in $Path) { + $files = Get-ChildItem "$directory\*.xml" + + if ($Template) { + $files = $files | Where-Object BaseName -in $Template + } + + foreach ($file in $files) { + try { + $xml = [xml](Get-Content $file) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + foreach ($session in $xml.event_sessions) { + $meta = $metadata | Where-Object Name -eq $session.event_session.name + if ($Pattern) { + if ( + # There's probably a better way to do this + ($session.event_session.name -match $Pattern) -or + ($session.event_session.TemplateCategory.'#text' -match $Pattern) -or + ($session.event_session.TemplateSource -match $Pattern) -or + ($session.event_session.TemplateDescription.'#text' -match $Pattern) -or + ($session.event_session.TemplateName.'#text' -match $Pattern) -or + ($meta.Source -match $Pattern) + ) { + [pscustomobject]@{ + Name = $session.event_session.name + Category = $session.event_session.TemplateCategory.'#text' + Source = $meta.Source + Compatibility = ("$($meta.Compatibility)").ToString().Replace(",", "") + Description = $session.event_session.TemplateDescription.'#text' + TemplateName = $session.event_session.TemplateName.'#text' + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, TemplateName, Path + } + } + else { + [pscustomobject]@{ + Name = $session.event_session.name + Category = $session.event_session.TemplateCategory.'#text' + Source = $meta.Source + Compatibility = $meta.Compatibility.ToString().Replace(",", "") + Description = $session.event_session.TemplateDescription.'#text' + TemplateName = $session.event_session.TemplateName.'#text' + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, TemplateName, Path + } + } + } + } + } +} +function Get-DbaXESmartTarget { + <# + .SYNOPSIS + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + .DESCRIPTION + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/Get-DbaXESmartTarget + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + Get-DbaXESmartTarget + + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + #> + [CmdletBinding()] + param ( + [switch]$EnableException + ) + process { + try { + Get-Job | Where-Object Name -Match SmartTarget | Select-Object -Property ID, Name, State + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } +} +function Get-DbaXEStore { + <# + .SYNOPSIS + Get a Extended Events store + + .DESCRIPTION + Get a Extended Events store + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXEStore + + .EXAMPLE + Get-DbaXEStore -SqlInstance ServerA\sql987 + + Returns an XEvent Store. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Select-DefaultView -InputObject $store -Property ComputerName, InstanceName, SqlInstance, ServerName, Sessions, Packages, RunningSessionCount + } + } +} +function Import-DbaCsvToSql { + <# + .SYNOPSIS + Efficiently imports very large (and small) CSV files into SQL Server using only the .NET Framework and PowerShell. + + .DESCRIPTION + Import-DbaCsvToSql takes advantage of .NET's super fast SqlBulkCopy class to import CSV files into SQL Server at up to 90,000 rows a second. + + The entire import is contained within a transaction, so if a failure occurs or the script is aborted, no changes will persist. + + If the table specified does not exist, it will be automatically created using best guessed data types. In addition, the destination table can be truncated prior to import. + + The Query parameter will be used to import only the data returned from a SQL Query executed against the CSV file(s). This function supports a number of bulk copy options. Please see parameter list for details. + + .PARAMETER CSV + Specifies path to the CSV file(s) to be imported. Multiple files may be imported if they are formatted similarly. + + If no file is specified, a dialog box will appear to select your file(s). + + .PARAMETER FirstRowColumns + If this switch is enabled, the first row in the file will be used as column names for the data being imported. + + If the first row does not contain column names and -Query is specified, use field names "column1, column2, column3" and so on. + + .PARAMETER Delimiter + Specifies the delimiter used in the imported file(s). If no delimiter is specified, comma is assumed. + + Valid delimiters are '`t`, '|', ';',' ' and ',' (tab, pipe, semicolon, space, and comma). + + .PARAMETER SingleColumn + Specifies that the file contains a single column of data + + .PARAMETER SqlInstance + The SQL Server Instance to import data into. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database the CSV will be imported into. Options for this this parameter are auto-populated from the server. + + .PARAMETER Schema + Specifies the schema in which the SQL table or view where CSV will be imported into resides. Default is dbo + + If a schema name is not specified, and a CSV name with multiple dots is specified (ie; something.data.csv) then this will be interpreted as a request to import into a table [data] in the schema [something]. + + If a schema does not currently exist, it will be created, after a prompt to confirm this. Authorization will be set to dbo by default + + .PARAMETER Table + Specifies the SQL table or view where CSV will be imported into. + + If a table name is not specified, the table name will be automatically determined from the filename, and a prompt will appear to confirm the table name. + + If a table does not currently exist, it will created. SQL datatypes are determined from the first row of the CSV that contains data (skips first row if -FirstRowColumns is specified). Datatypes used are: bigint, numeric, datetime and varchar(MAX). + + If the automatically generated table datatypes do not work for you, please create the table prior to import. + + .PARAMETER Truncate + If this switch is enabled, the destination table will be truncated prior to import. + + .PARAMETER Safe + If this switch is enabled, OleDb is used to import the records. By default, Import-DbaCsvToSql uses StreamReader for imports. StreamReader is super fast, but may not properly parse some files. + + When using OleDb the import will be slower but more predictable when it comes to parsing CSV files. A schema.ini is automatically generated for best results. If schema.ini currently exists in the directory, it will be moved to a temporary location, then moved back. + + OleDB also enables the script to use the -Query parameter, which enables you to import specific subsets of data within a CSV file. OleDB imports at up to 21,000 rows/sec. + + .PARAMETER Turbo + If this switch is enabled, a Table Lock will be created for the import to make the import run as fast as possible. Depending upon the number of columns and datatypes, this may be over 90,000 records per second. + + This switch cannot be used in conjunction with -Query. + + Remember the Turbo button? This one actually works. Turbo is mega fast, but may not handle some datatypes as well as other methods. + + If your CSV file is rather vanilla and doesn't have a ton of NULLs, Turbo may work well for you. + + .PARAMETER First + Specifies the number of rows to import. If this parameter is omitted, the entire file is imported. Row counts start at the top of the file, but skip the first row if -FirstRowColumns is specified. + + Use -Query if you need advanced First (TOP) functionality. + + .PARAMETER Query + Specifies a query to execute against the CSV data to select/modify the data being imported. + + To make command line queries easy, this module will convert the word "csv" to the actual CSV formatted table name. If the FirstRowColumns switch is not used, the query should use column1, column2, column3, etc. + + Cannot be used in conjunction with -Turbo or -First. When -Query is specified, the slower import method, OleDb, will be used. + + .PARAMETER NotifyAfter + Specifies the import row count interval for reporting progress. A notification will be shown after each group of this many rows has been imported. + + .PARAMETER BatchSize + Specifies the batch size for the import. Defaults to 50000. + + .PARAMETER TableLock + If this switch is enabled, the SqlBulkCopy option to acquire a table lock will be used. This is automatically used if -Turbo is enabled. + + Per Microsoft "Obtain a bulk update lock for the duration of the bulk copy operation. When not + specified, row locks are used." + + .PARAMETER CheckConstraints + If this switch is enabled, the SqlBulkCopy option to check constraints will be used. + + Per Microsoft "Check constraints while data is being inserted. By default, constraints are not checked." + + .PARAMETER FireTriggers + If this switch is enabled, the SqlBulkCopy option to allow insert triggers to be executed will be used. + + Per Microsoft "When specified, cause the server to fire the insert triggers for the rows being inserted into the database." + + .PARAMETER KeepIdentity + If this switch is enabled, the SqlBulkCopy option to keep identity values from the source will be used. + + Per Microsoft "Preserve source identity values. When not specified, identity values are assigned by the destination." + + .PARAMETER KeepNulls + If this switch is enabled, the SqlBulkCopy option to keep NULL values in the table will be used. + + Per Microsoft "Preserve null values in the destination table regardless of the settings for default values. When not specified, null values are replaced by default values where applicable." + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://blog.netnerds.net/2015/09/Import-DbaCsvtosql-super-fast-csv-to-sql-server-import-powershell-module/ + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv to the SQL "markets" database on a SQL Server named sql001. + + Since a table name was not specified, the table name is automatically determined from filename as "housing" and a prompt will appear to confirm table name. + + The first row is not skipped, as it does not contain column names. + + .EXAMPLE + Import-DbaCsvToSql -Csv .\housing.csv -SqlInstance sql001 -Database markets -Table housing -First 100000 -Safe -Delimiter "`t" -FirstRowColumns + + Imports the first 100,000 rows of the tab delimited housing.csv file to the "housing" table in the "markets" database on a SQL Server named sql001. Since -Safe was specified, the OleDB method will be used for the bulk import. The first row is skipped, as it contains column names. + + .EXAMPLE + Import-DbaCsvToSql -csv C:\temp\huge.txt -SqlInstance sqlcluster -Database locations -Table latitudes -Delimiter "|" -Turbo + + Imports all records from the pipe delimited huge.txt file using the fastest method possible into the latitudes table within the locations database. Obtains a table lock for the duration of the bulk copy operation. This specific command has been used + to import over 10.5 million rows in 2 minutes. + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv, .\housing2.csv -SqlInstance sql001 -Database markets -Table housing -Delimiter "`t" -query "select top 100000 column1, column3 from csv" -Truncate + + Truncates the "housing" table, then imports columns 1 and 3 of the first 100000 rows of the tab-delimited housing.csv in the C:\temp directory, and housing2.csv in the current directory. Since the query is executed against both files, a total of 200,000 rows will be imported. + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv -SqlInstance sql001 -Database markets -Table housing -query "select address, zip from csv where state = 'Louisiana'" -FirstRowColumns -Truncate -FireTriggers + + Uses the first line to determine CSV column names. Truncates the "housing" table on the SQL Server, then imports the address and zip columns from all records in the housing.csv where the state equals Louisiana. + + Triggers are fired for all rows. Note that this does slightly slow down the import. + + .EXAMPLE + Import-DbaCsvToSql -Csv c:\temp\SingleColumn.csv -SqlInstance sql001 -Database markets -Table TempTable -SingleColumn + + Upload the single column Csv SingleColumn.csv to Temptable which has just one column + + .EXAMPLE + Import-DbaCsvToSql -Csv "\\FileServer\To Import\housing.csv" -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv located in the share named "To Import" on FileServer to the SQL "markets" database on a SQL Server named sql001. + + .EXAMPLE + Import-DbaCsvToSql -Csv '\\FileServer\R$\To Import\housing.csv' -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv located in the directory R:\To Import on FileServer using the administrative share to the SQL "markets" database on a SQL Server named sql001. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [object]$SqlCredential, + [string]$Table, + [string]$Schema = "dbo", + [switch]$Truncate, + [ValidateSet("`t", "|", ";", " ", ",")] + [string]$Delimiter = ",", + [switch]$SingleColumn, + [switch]$FirstRowColumns, + [parameter(ParameterSetName = "reader")] + [switch]$Turbo, + [parameter(ParameterSetName = "ole")] + [switch]$Safe, + [int]$First = 0, + [parameter(ParameterSetName = "ole")] + [string]$Query = "select * from csv", + [int]$BatchSize = 50000, + [int]$NotifyAfter, + [switch]$TableLock, + [switch]$CheckConstraints, + [switch]$FireTriggers, + [switch]$KeepIdentity, + [switch]$KeepNulls, + #[Parameter(DontShow)] + [switch]$shellswitch, + #[Parameter(DontShow)] + [string]$SqlCredentialPath + ) + + DynamicParam { + + if ($SqlInstance.length -gt 0) { + # Auto populate database list from specified sqlserver + $paramconn = New-Object System.Data.SqlClient.SqlConnection + + if ($SqlCredentialPath.length -gt 0) { + $SqlCredential = Import-CliXml $SqlCredentialPath + } + + if ($SqlCredential.count -eq 0 -or $null -eq $SqlCredential) { + $paramconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;" + } + else { + $paramconn.ConnectionString = "Data Source=$SqlInstance;User Id=$($SqlCredential.UserName); Password=$($SqlCredential.GetNetworkCredential().Password);" + } + + try { + $paramconn.Open() + $sql = "select name from master.dbo.sysdatabases" + $paramcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $paramconn, $null) + $paramdt = New-Object System.Data.DataTable + $paramdt.Load($paramcmd.ExecuteReader()) + $databaselist = $paramdt.rows.name + $null = $paramcmd.Dispose() + $null = $paramconn.Close() + $null = $paramconn.Dispose() + } + catch { + # But if the routine fails, at least let them specify a database manually + $databaselist = "" + } + + # Reusable parameter setup + $newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $attributes = New-Object System.Management.Automation.ParameterAttribute + $attributes.Mandatory = $false + + # Database list parameter setup + $dbattributes = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] + $dbattributes.Add($attributes) + # If a list of databases were returned, populate the parameter set + if ($databaselist.length -gt 0) { + $dbvalidationset = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $databaselist + $dbattributes.Add($dbvalidationset) + } + + $Database = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Database", [String], $dbattributes) + $newparams.Add("Database", $Database) + return $newparams + } + } + + begin { + function Get-Columns { + <# + .SYNOPSIS + TextFieldParser will be used instead of an OleDbConnection. + This is because the OleDbConnection driver may not exist on x64. + + .EXAMPLE + $columns = Get-Columns -Csv .\myfile.csv -Delimiter "," -FirstRowColumns $true + + .OUTPUTS + Array of column names + #> + + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [Parameter(Mandatory = $true)] + [bool]$FirstRowColumns + ) + + $columnparser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($csv[0]) + $columnparser.TextFieldType = "Delimited" + $columnparser.SetDelimiters($Delimiter) + $rawcolumns = $columnparser.ReadFields() + + if ($FirstRowColumns -eq $true) { + $columns = ($rawcolumns | ForEach-Object { $_ -Replace '"' } | Select-Object -Property @{ Name = "name"; Expression = { "[$_]" } }).name + } + else { + $columns = @() + foreach ($number in 1..$rawcolumns.count) { + $columns += "[column$number]" + } + } + + $columnparser.Close() + $columnparser.Dispose() + return $columns + } + + function Get-ColumnText { + <# + .SYNOPSIS + Returns an array of data, which can later be parsed for potential datatypes. + + .EXAMPLE + $columns = Get-Columns -Csv .\myfile.csv -Delimiter "," + + .OUTPUTS + Array of column data + #> + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter + ) + $columnparser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($csv[0]) + $columnparser.TextFieldType = "Delimited" + $columnparser.SetDelimiters($Delimiter) + $line = $columnparser.ReadLine() + # Skip a line, in case first line are column names + $line = $columnparser.ReadLine() + $datatext = $columnparser.ReadFields() + $columnparser.Close() + $columnparser.Dispose() + return $datatext + } + + function Write-Schemaini { + <# + .SYNOPSIS + Unfortunately, passing delimiter within the OleDBConnection connection string is unreliable, so we'll use schema.ini instead. The default delimiter in Windows changes depending on country, so we'll do this for every delimiter, even commas. + + Get OLE datatypes based on best guess of column data within the -Columns parameter. + + Sometimes SQL will accept a datetime that OLE won't, so Text will be used for datetime. + + .EXAMPLE + $columns = Get-Columns -Csv C:\temp\myfile.csv -Delimiter "," + $movedschemainis = Write-Schemaini -Csv C:\temp\myfile.csv -Columns $columns -ColumnText $columntext -Delimiter "," -FirstRowColumns $true + + .OUTPUTS + Creates new schema files, that look something like this: + + [housingdata.csv] + Format=Delimited(,) + ColNameHeader=True + Col1="House ID" Long + Col2="Description" Memo + Col3="Price" Double + + Returns an array of existing schema files that have been moved, if any. + #> + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string[]]$Columns, + [string[]]$ColumnText, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [Parameter(Mandatory = $true)] + [bool]$FirstRowColumns + ) + + $movedschemainis = @{ } + foreach ($file in $csv) { + $directory = Split-Path $file + $schemaexists = Test-Path "$directory\schema.ini" + if ($schemaexists -eq $true) { + $newschemaname = "$env:TEMP\$(Split-Path $file -leaf)-schema.ini" + $movedschemainis.Add($newschemaname, "$directory\schema.ini") + Move-Item "$directory\schema.ini" $newschemaname -Force + } + + $filename = Split-Path $file -leaf; $directory = Split-Path $file + Add-Content -Path "$directory\schema.ini" -Value "[$filename]" + Add-Content -Path "$directory\schema.ini" -Value "Format=Delimited($InternalDelimiter)" + Add-Content -Path "$directory\schema.ini" -Value "ColNameHeader=$FirstRowColumns" + + $index = 0 + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]", '"' }) + + foreach ($datatype in $columntext) { + $olecolumnname = $olecolumns[$index] + $index++ + + try { + [System.Guid]::Parse($datatype) | Out-Null; $isguid = $true + } + catch { + $isguid = $false + } + + if ($isguid -eq $true) { + $oledatatype = "Text" + } + elseif ([int64]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Long" + } + elseif ([double]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Double" + } + elseif ([datetime]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Text" + } + else { + $oledatatype = "Memo" + } + + Add-Content -Path "$directory\schema.ini" -Value "Col$($index)`=$olecolumnname $oledatatype" + } + } + return $movedschemainis + } + + function New-SqlTable { + <# + .SYNOPSIS + Creates new Table using existing SqlCommand. + + SQL datatypes based on best guess of column data within the -ColumnText parameter. + Columns parameter determine column names. + + .EXAMPLE + New-SqlTable -Csv $Csv -Delimiter $InternalDelimiter -Columns $columns -ColumnText $columntext -SqlConn $sqlconn -Transaction $transaction + + .OUTPUTS + Creates new table + #> + + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [string[]]$Columns, + [string[]]$ColumnText, + [System.Data.SqlClient.SqlConnection]$sqlconn, + [System.Data.SqlClient.SqlTransaction]$transaction + ) + # Get SQL datatypes by best guess on first data row + $sqldatatypes = @(); $index = 0 + + foreach ($column in $columntext) { + $sqlcolumnname = $Columns[$index] + $index++ + + # bigint, float, and datetime are more accurate, but it didn't work + # as often as it should have, so we'll just go for a smaller datatype + if ([int64]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + elseif ([double]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + elseif ([datetime]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + else { + $sqldatatype = "varchar(MAX)" + } + + $sqldatatypes += "$sqlcolumnname $sqldatatype" + } + + $sql = "BEGIN CREATE TABLE [$schema].[$table] ($($sqldatatypes -join ' NULL,')) END" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + $errormessage = $_.Exception.Message.ToString() + throw "Failed to execute $sql. `nDid you specify the proper delimiter? `n$errormessage" + } + + Write-Output "[*] Successfully created table $schema.$table with the following column definitions:`n $($sqldatatypes -join "`n ")" + # Write-Warning "All columns are created using a best guess, and use their maximum datatype." + Write-Warning "This is inefficient but allows the script to import without issues." + Write-Warning "Consider creating the table first using best practices if the data will be used in production." + } + + + if ($shellswitch -eq $false) { Write-Output "[*] Started at $(Get-Date)" } + + # Load the basics + [void][Reflection.Assembly]::LoadWithPartialName("System.Data") + [void][Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") + [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") + + # Getting the total rows copied is a challenge. Use SqlBulkCopyExtension. + # http://stackoverflow.com/questions/1188384/sqlbulkcopy-row-count-when-complete + + $source = 'namespace System.Data.SqlClient + { + using Reflection; + + public static class SqlBulkCopyExtension + { + const String _rowsCopiedFieldName = "_rowsCopied"; + static FieldInfo _rowsCopiedField = null; + + public static int RowsCopiedCount(this SqlBulkCopy bulkCopy) + { + if (_rowsCopiedField == null) _rowsCopiedField = typeof(SqlBulkCopy).GetField(_rowsCopiedFieldName, BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance); + return (int)_rowsCopiedField.GetValue(bulkCopy); + } + } + } + ' + Add-Type -ReferencedAssemblies 'System.Data.dll' -TypeDefinition $source -ErrorAction SilentlyContinue + } + + process { + # turbo mode requires a table lock, or it's just regular fast + if ($turbo -eq $true) { + $tablelock = $true + } + + # Hack to get around the delimter parameter ValidateSet + if ($SingleColumn -eq $true) { + $InternalDelimiter = '' + } + else { + $InternalDelimiter = $Delimiter + } + + # The query parameter requires OleDB which is invoked by the "safe" variable + # Actually, a select could be performed on the datatable used in StreamReader, too. + # Maybe that will be done later. + if ($query -ne "select * from csv") { + $safe = $true + } + + if ($first -gt 0 -and $query -ne "select * from csv") { + throw "Cannot use both -Query and -First. If a query is necessary, use TOP $first within your SQL statement." + } + + # In order to support -First in both Streamreader, and OleDb imports, the query must be modified slightly. + if ($first -gt 0) { + $query = "select top $first * from csv" + } + + # If shell switch occured, and encrypted SQL credentials were written to disk, create $SqlCredential + if ($SqlCredentialPath.length -gt 0) { + $SqlCredential = Import-CliXml $SqlCredentialPath + } + + # Get Database string from RuntimeDefinedParameter if required + if ($database -isnot [string]) { + $database = $PSBoundParameters.Database + } + if ($database.length -eq 0) { + throw "You must specify a database." + } + + # Check to ensure a Windows account wasn't used as a SQL Credential + if ($SqlCredential.count -gt 0 -and $SqlCredential.UserName -like "*\*") { + throw "Only SQL Logins can be used as a SqlCredential." + } + + # If no CSV was specified, prompt the user to select one. + if ($csv.length -eq 0) { + $fd = New-Object System.Windows.Forms.OpenFileDialog + $fd.InitialDirectory = [environment]::GetFolderPath("MyDocuments") + $fd.Filter = "CSV Files (*.csv;*.tsv;*.txt)|*.csv;*.tsv;*.txt" + $fd.Title = "Select one or more CSV files" + $fd.MultiSelect = $true + $null = $fd.showdialog() + $csv = $fd.filenames + if ($csv.length -eq 0) { + throw "No CSV file selected." + } + } + else { + foreach ($file in $csv) { + $exists = Test-Path $file + if ($exists -eq $false) { + throw "$file does not exist" + } + } + } + + # Resolve the full path of each CSV + $resolvedcsv = @() + foreach ($file in $csv) { + $resolvedcsv += (Resolve-Path $file).ProviderPath + } + $csv = $resolvedcsv + + # UniqueIdentifier kills OLE DB / SqlBulkCopy imports. Check to see if destination table contains this datatype. + if ($safe -eq $true) { + $sqlcheckconn = New-Object System.Data.SqlClient.SqlConnection + if ($SqlCredential.count -eq 0 -or $null -eq $SqlCredential) { + $sqlcheckconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;Connection Timeout=3; Initial Catalog=master" + } + else { + $username = ($SqlCredential.UserName).TrimStart("\") + $sqlcheckconn.ConnectionString = "Data Source=$SqlInstance;User Id=$username; Password=$($SqlCredential.GetNetworkCredential().Password);Connection Timeout=3; Initial Catalog=master" + } + + try { + $sqlcheckconn.Open() + } + catch { + throw $_.Exception + } + + # Ensure database exists + $sql = "select count(*) from master.dbo.sysdatabases where name = '$database'" + $sqlcheckcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlcheckconn) + $dbexists = $sqlcheckcmd.ExecuteScalar() + if ($dbexists -eq $false) { + throw "Database does not exist on $SqlInstance" + } + + # Change database after the fact, because if db doesn't exist, the login would fail. + $sqlcheckconn.ChangeDatabase($database) + + $sql = "SELECT t.name as datatype FROM sys.columns c + JOIN sys.types t ON t.system_type_id = c.system_type_id + WHERE c.object_id = object_id('$schema.$table') and t.name != 'sysname'" + $sqlcheckcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlcheckconn) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.load($sqlcheckcmd.ExecuteReader("CloseConnection")) + $sqlcheckconn.Dispose() + if ($sqlcolumns.datatype -contains "UniqueIdentifier") { + throw "UniqueIdentifier not supported by OleDB/SqlBulkCopy. Query and Safe cannot be supported." + } + } + + if ($safe -eq $true) { + # Check for drivers. First, ACE (Access) if file is smaller than 2GB, then JET + # ACE doesn't handle files larger than 2gb. What gives? + foreach ($file in $csv) { + $filesize = (Get-ChildItem $file).Length / 1GB + if ($filesize -gt 1.99) { + $jetonly = $true + } + } + + if ($jetonly -ne $true) { + $provider = (New-Object System.Data.OleDb.OleDbEnumerator).GetElements() | Where-Object { $_.SOURCES_NAME -like "Microsoft.ACE.OLEDB.*" } + } + + if ($null -eq $provider) { + $provider = (New-Object System.Data.OleDb.OleDbEnumerator).GetElements() | Where-Object { $_.SOURCES_NAME -like "Microsoft.Jet.OLEDB.*" } + } + + # If a suitable provider cannot be found (If x64 and Access hasn't been installed) + # switch to x86, because it natively supports JET + if ($null -ne $provider) { + if ($provider -is [system.array]) { + $provider = $provider[$provider.GetUpperBound(0)].SOURCES_NAME + } + else { + $provider = $provider.SOURCES_NAME + } + } + + # If a provider doesn't exist, it is necessary to switch to x86 which natively supports JET. + if ($null -eq $provider) { + # While Install-Module takes care of installing modules to x86 and x64, Import-Module doesn't. + # Because of this, the Module must be exported, written to file, and imported in the x86 shell. + $definition = (Get-Command Import-DbaCsvToSql).Definition + $function = "Function Import-DbaCsvToSql { $definition }" + Set-Content "$env:TEMP\Import-DbaCsvToSql.psm1" $function + + # Encode the SQL string, since some characters may mess up after being passed a second time. + $bytes = [System.Text.Encoding]::UTF8.GetBytes($query) + $query = [System.Convert]::ToBase64String($bytes) + + # Put switches back into proper format + $switches = @() + $options = "TableLock", "CheckConstraints", "FireTriggers", "KeepIdentity", "KeepNulls", "Default", "Truncate", "FirstRowColumns", "Safe" + foreach ($option in $options) { + $optionValue = Get-Variable $option -ValueOnly -ErrorAction SilentlyContinue + if ($optionValue -eq $true) { + $switches += "-$option" + } + } + + # Perform the actual switch, which removes any registered Import-DbaCsvToSql modules + # Then imports, and finally re-executes the command. + $csv = $csv -join ","; $switches = $switches -join " " + if ($SqlCredential.count -gt 0) { + $SqlCredentialPath = "$env:TEMP\sqlcredential.xml" + Export-CliXml -InputObject $SqlCredential $SqlCredentialPath + } + $command = "Import-DbaCsvToSql -Csv $csv -SqlInstance '$SqlInstance'-Database '$database' -Table '$table' -Delimiter '$InternalDelimiter' -First $First -Query '$query' -Batchsize $BatchSize -NotifyAfter $NotifyAfter $switches -shellswitch" + + if ($SqlCredentialPath.length -gt 0) { + $command += " -SqlCredentialPath $SqlCredentialPath" + } + Write-Verbose "Switching to x86 shell, then switching back." + &"$env:windir\syswow64\windowspowershell\v1.0\powershell.exe" "$command" + return + } + } + + # Do the first few lines contain the specified delimiter? + foreach ($file in $csv) { + try { $firstfewlines = Get-Content $file -First 3 -ErrorAction Stop } + catch { throw "$file is in use." } + if ($SingleColumn -ne $true ) { + foreach ($line in $firstfewlines) { + if (($line -match $InternalDelimiter) -eq $false) { + throw "Delimiter $InternalDelimiter not found in first row of $file." + } + } + } + } + + # If more than one csv specified, check to ensure number of columns match + if ($csv -is [system.array]) { + if ($SingleColumn -ne $true) { + $numberofcolumns = ((Get-Content $csv[0] -First 1 -ErrorAction Stop) -Split $InternalDelimiter).Count + + foreach ($file in $csv) { + $firstline = Get-Content $file -First 1 -ErrorAction Stop + $newnumcolumns = ($firstline -Split $InternalDelimiter).Count + if ($newnumcolumns -ne $numberofcolumns) { + throw "Multiple csv file mismatch. Do both use the same delimiter and have the same number of columns?" + } + } + } + } + + # Automatically generate Table name if not specified, then prompt user to confirm + if ($table.length -eq 0) { + $table = [IO.Path]::GetFileNameWithoutExtension($csv[0]) + + #Count the dots in the file name. + #1 dot, treat it as schema.table naming + #2 or more dots, really should catch it as bad practice, but the rest of the script appears to let it pass + if (($table.ToCharArray() | Where-Object {$_ -eq '.'} | Measure-Object).count -gt 0) { + if (($schema -ne $table.Split('.')[0]) -and ($schema -ne 'dbo')) { + $title = "Conflicting schema names specified" + $message = "Please confirm which schema you want to use." + $schemaA = New-Object System.Management.Automation.Host.ChoiceDescription "&A - $schema", "Use schema name $schema for import." + $schemaB = New-Object System.Management.Automation.Host.ChoiceDescription "&B - $($table.Split('.')[0])", "Use schema name $($table.Split('.')[0]) for import." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($schemaA, $schemaB) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + if ($result -eq 1) { + $schema = $table.Split('.')[0] + $tmparray = $table.split('.') + $table = $tmparray[1..$tmparray.Length] -join '.' + } + } + + } + else { + $title = "Table name not specified." + $message = "Would you like to use the automatically generated name: $table" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Uses table name $table for import." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Allows you to specify an alternative table name." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + if ($result -eq 1) { + do { + $table = Read-Host "Please enter a table name" + } + while ($table.Length -eq 0) + } + + } + } + + # If the shell has switched, decode the $query string. + if ($shellswitch -eq $true) { + $bytes = [System.Convert]::FromBase64String($Query) + $query = [System.Text.Encoding]::UTF8.GetString($bytes) + $csv = $csv -Split "," + } + + # Create columns based on first data row of first csv. + if ($SingleColumn -ne $true) { + Write-Output "[*] Calculating column names and datatypes" + $columns = Get-Columns -Csv $Csv -Delimiter $InternalDelimiter -FirstRowColumns $FirstRowColumns + if ($columns.count -gt 255 -and $safe -eq $true) { + throw "CSV must contain fewer than 256 columns." + } + } + + if ($SingleColumn -ne $true) { + $columntext = Get-ColumnText -Csv $Csv -Delimiter $InternalDelimiter + } + + # OLEDB method requires extra checks + if ($safe -eq $true) { + # Advanced SQL queries may not work (SqlBulkCopy likes a 1 to 1 mapping), so warn the user. + if ($Query -match "GROUP BY" -or $Query -match "COUNT") { + Write-Warning "Script doesn't really support the specified query. This probably won't work, but will be attempted anyway." + } + + # Check for proper SQL syntax, which for the purposes of this module must include the word "table" + if ($query.ToLower() -notmatch "\bcsv\b") { + throw "SQL statement must contain the word 'csv'. Please see this module's documentation for more details." + } + + # In order to ensure consistent results, a schema.ini file must be created. + # If a schema.ini already exists, it will be moved to TEMP temporarily. + Write-Verbose "Creating schema.ini" + $movedschemainis = Write-Schemaini -Csv $Csv -Columns $columns -Delimiter "$InternalDelimiter" -FirstRowColumns $FirstRowColumns -ColumnText $columntext + } + + # Display SQL Server Login info + if ($sqlcredential.count -gt 0) { + $username = "SQL login $($SqlCredential.UserName)" + } + else { + $username = "Windows login $(whoami)" + } + # Open Connection to SQL Server + Write-Output "[*] Logging into $SqlInstance as $username" + $sqlconn = New-Object System.Data.SqlClient.SqlConnection + if ($SqlCredential.count -eq 0) { + $sqlconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;Connection Timeout=3; Initial Catalog=master" + } + else { + $sqlconn.ConnectionString = "Data Source=$SqlInstance;User Id=$($SqlCredential.UserName); Password=$($SqlCredential.GetNetworkCredential().Password);Connection Timeout=3; Initial Catalog=master" + } + + try { + $sqlconn.Open() + } + catch { + throw "Could not open SQL Server connection. Is $SqlInstance online?" + } + + # Everything will be contained within 1 transaction, even creating a new table if required + # and truncating the table, if specified. + $transaction = $sqlconn.BeginTransaction() + + # Ensure database exists + $sql = "select count(*) from master.dbo.sysdatabases where name = '$database'" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $dbexists = $sqlcmd.ExecuteScalar() + if ($dbexists -eq $false) { + throw "Database does not exist on $SqlInstance" + } + Write-Output "[*] Database exists" + + $sqlconn.ChangeDatabase($database) + + # Enure Schema exists + $sql = "select count(*) from $database.sys.schemas where name='$schema'" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $schemaexists = $sqlcmd.ExecuteScalar() + + # If Schema doesn't exist create it + # Defaulting to dbo. + if ($schemaexists -eq $false) { + Write-Output "[*] Creating schema $schema" + $sql = "CREATE SCHEMA [$schema] AUTHORIZATION dbo" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + Write-Warning "Could not create $schema" + } + + } + + # Ensure table exists + $sql = "select count(*) from $database.sys.tables where name = '$table' and schema_id=schema_id('$schema')" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $tablexists = $sqlcmd.ExecuteScalar() + + # Create the table if required. Remember, this will occur within a transaction, so if the script fails, the + # new table will no longer exist. + if ($tablexists -eq $false) { + Write-Output "[*] Table does not exist" + Write-Output "[*] Creating table" + New-SqlTable -Csv $Csv -Delimiter $InternalDelimiter -Columns $columns -ColumnText $columntext -SqlConn $sqlconn -Transaction $transaction + } + else { + Write-Output "[*] Table exists" + } + + # Truncate if specified. Remember, this will occur within a transaction, so if the script fails, the + # truncate will not be committed. + if ($truncate -eq $true) { + Write-Output "[*] Truncating table" + $sql = "TRUNCATE TABLE [$schema].[$table]" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + Write-Warning "Could not truncate $schema.$table" + } + } + + # Get columns for column mapping + if ($null -eq $columnMappings) { + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]" }) + $sql = "select name from sys.columns where object_id = object_id('$schema.$table') order by column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.Load($sqlcmd.ExecuteReader()) + } + + # Time to import! + $elapsed = [System.Diagnostics.Stopwatch]::StartNew() + + # Process each CSV file specified + foreach ($file in $csv) { + + # Dynamically set NotifyAfter if it wasn't specified + if ($notifyAfter -eq 0) { + if ($resultcount -is [int]) { + $notifyafter = $resultcount / 10 + } + else { + $notifyafter = 50000 + } + } + + # Setup bulk copy + Write-Output "[*] Starting bulk copy for $(Split-Path $file -Leaf)" + + # Setup bulk copy options + $bulkCopyOptions = @() + $options = "TableLock", "CheckConstraints", "FireTriggers", "KeepIdentity", "KeepNulls", "Default", "Truncate" + foreach ($option in $options) { + $optionValue = Get-Variable $option -ValueOnly -ErrorAction SilentlyContinue + if ($optionValue -eq $true) { + $bulkCopyOptions += "$option" + } + } + $bulkCopyOptions = $bulkCopyOptions -join " & " + + # Create SqlBulkCopy using default options, or options specified in command line. + if ($bulkCopyOptions.count -gt 1) { + $bulkcopy = New-Object Data.SqlClient.SqlBulkCopy($oleconnstring, $bulkCopyOptions, $transaction) + } + else { + $bulkcopy = New-Object Data.SqlClient.SqlBulkCopy($sqlconn, "Default", $transaction) + } + + $bulkcopy.DestinationTableName = "[$schema].[$table]" + $bulkcopy.bulkcopyTimeout = 0 + $bulkCopy.BatchSize = $BatchSize + $bulkCopy.NotifyAfter = $NotifyAfter + + if ($safe -eq $true) { + # Setup bulkcopy mappings + for ($columnid = 0; $columnid -lt $sqlcolumns.rows.count; $columnid++) { + $null = $bulkCopy.ColumnMappings.Add($olecolumns[$columnid], $sqlcolumns.rows[$columnid].ItemArray[0]) + } + + # Setup the connection string. Data Source is the directory that contains the csv. + # The file name is also the table name, but with a "#" instead of a "." + $datasource = Split-Path $file + $tablename = (Split-Path $file -leaf).Replace(".", "#") + $oleconnstring = "Provider=$provider;Data Source=$datasource;Extended Properties='text';" + + # To make command line queries easier, let the user just specify "csv" instead of the + # OleDbconnection formatted name (file.csv -> file#csv) + $sql = $Query -replace "\bcsv\b", " [$tablename]" + + # Setup the OleDbconnection + $oleconn = New-Object System.Data.OleDb.OleDbconnection + $oleconn.ConnectionString = $oleconnstring + + # Setup the OleDBCommand + $olecmd = New-Object System.Data.OleDB.OleDBCommand + $olecmd.Connection = $oleconn + $olecmd.CommandText = $sql + + try { + $oleconn.Open() + } + catch { + throw "Could not open OLEDB connection." + } + + # Attempt to get the number of results so that a nice progress bar can be displayed. + # This takes extra time, and files over 100MB take too long, so just skip them. + if ($sql -match "GROUP BY") { + Write-Warning -Message "Query contains GROUP BY clause. Skipping result count." + } + else { + Write-Output "[*] Determining total rows to be copied. This may take a few seconds." + } + + if ($sql -match "\bselect top\b") { + try { + $split = $sql -split "\bselect top \b" + $resultcount = [int]($split[1].Trim().Split()[0]) + Write-Output "[*] Attempting to fetch $resultcount rows" + } + catch { + Write-Warning "Couldn't determine total rows to be copied." + } + } + elseif ($sql -notmatch "GROUP BY") { + $filesize = (Get-ChildItem $file).Length / 1MB + if ($filesize -lt 100) { + try { + $split = $sql -split "\bfrom\b" + $sqlcount = "select count(*) from $($split[1])" + # Setup the OleDBCommand + $olecmd = New-Object System.Data.OleDB.OleDBCommand + $olecmd.Connection = $oleconn + $olecmd.CommandText = $sqlcount + $resultcount = [int]($olecmd.ExecuteScalar()) + Write-Output "[*] $resultcount rows will be copied" + } + catch { + Write-Warning "Couldn't determine total rows to be copied" + } + } + else { + Write-Output "[*] File is too large for efficient result count; progress bar will not be shown." + } + } + } + + # Write to server :D + try { + if ($safe -ne $true) { + # Check to ensure batchsize isn't equal to 0 + if ($batchsize -eq 0) { + write-warning "Invalid batchsize for this operation. Increasing to 50k" + $batchsize = 50000 + } + + # Open the text file from disk + $reader = New-Object System.IO.StreamReader($file) + if ($FirstRowColumns -eq $true) { + $null = $reader.readLine() + } + + # Create the reusable datatable. Columns will be genereated using info from SQL. + $datatable = New-Object System.Data.DataTable + + # Get table column info from SQL Server + $sql = "SELECT c.name as colname, t.name as datatype, c.max_length, c.is_nullable FROM sys.columns c + JOIN sys.types t ON t.system_type_id = c.system_type_id + WHERE c.object_id = object_id('$schema.$table') and t.name != 'sysname' + order by c.column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.load($sqlcmd.ExecuteReader()) + + foreach ($sqlcolumn in $sqlcolumns) { + $datacolumn = $datatable.Columns.Add() + $colname = $sqlcolumn.colname + $datacolumn.AllowDBNull = $sqlcolumn.is_nullable + $datacolumn.ColumnName = $colname + $datacolumn.DefaultValue = [DBnull]::Value + $datacolumn.Datatype = [string] + + # The following data types can sometimes cause issues when they are null + # so we will treat them differently + $convert = "bigint", "DateTimeOffset", "UniqueIdentifier", "smalldatetime", "datetime" + if ($convert -notcontains $sqlcolumn.datatype -and $turbo -ne $true) { + $null = $bulkCopy.ColumnMappings.Add($datacolumn.ColumnName, $sqlcolumn.colname) + } + } + # For the columns that cause trouble, we'll add an additional column to the datatable + # which will perform a conversion. + # Setting $column.datatype alone doesn't work as well as setting+converting. + if ($turbo -ne $true) { + $calcolumns = $sqlcolumns | Where-Object { $convert -contains $_.datatype } + foreach ($calcolumn in $calcolumns) { + $colname = $calcolumn.colname + $null = $newcolumn = $datatable.Columns.Add() + $null = $newcolumn.ColumnName = "computed$colname" + switch ($calcolumn.datatype) { + "bigint" { + $netdatatype = "System.Int64"; + $newcolumn.Datatype = [int64] + } + "DateTimeOffset" { + $netdatatype = "System.DateTimeOffset"; + $newcolumn.Datatype = [DateTimeOffset] + } + "UniqueIdentifier" { + $netdatatype = "System.Guid"; + $newcolumn.Datatype = [Guid] + } + {"smalldatetime", "datetime" -contains $_ } { + $netdatatype = "System.DateTime"; + $newcolumn.Datatype = [DateTime] + } + } + # Use a data column expression to facilitate actual conversion + $null = $newcolumn.Expression = "Convert($colname, $netdatatype)" + $null = $bulkCopy.ColumnMappings.Add($newcolumn.ColumnName, $calcolumn.colname) + } + } + + # Check to see if file has quote identified data (ie. "first","second","third") + $quoted = $false + $checkline = Get-Content $file -Last 1 + $checkcolumns = $checkline.Split($InternalDelimiter) + foreach ($checkcolumn in $checkcolumns) { + if ($checkcolumn.StartsWith('"') -and $checkcolumn.EndsWith('"')) { + $quoted = $true + } + } + + if ($quoted -eq $true) { + Write-Warning "The CSV file appears to use quoted identifiers. This may take a little longer." + # Thanks for this, Chris! http://www.schiffhauer.com/c-split-csv-values-with-a-regular-expression/ + $pattern = "((?<=`")[^`"]*(?=`"($InternalDelimiter|$)+)|(?<=$InternalDelimiter|^)[^$InternalDelimiter`"]*(?=$InternalDelimiter|$))" + } + if ($turbo -eq $true -and $first -eq 0) { + while ($null -ne ($line = $reader.ReadLine())) { + $i++ + if ($quoted -eq $true) { + $null = $datatable.Rows.Add(($line.TrimStart('"').TrimEnd('"')) -Split "`"$InternalDelimiter`"") + } + else { + $row = $datatable.Rows.Add($line.Split($InternalDelimiter)) + } + + if (($i % $batchsize) -eq 0) { + $bulkcopy.WriteToServer($datatable) + Write-Output "[*] $i rows have been inserted in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + $datatable.Clear() + } + } + } + else { + if ($turbo -eq $true -and $first -gt 0) { Write-Warning -Message "Using -First makes turbo a little slower." } + # Start import! + while ($null -ne ($line = $reader.ReadLine())) { + $i++ + try { + if ($quoted -eq $true) { + $row = $datatable.Rows.Add(($line.TrimStart('"').TrimEnd('"')) -Split $pattern) + } + else { + $row = $datatable.Rows.Add($line.Split($InternalDelimiter)) + } + } + catch { + $row = $datatable.NewRow() + try { + $tempcolumn = $line.Split($InternalDelimiter) + $colnum = 0 + foreach ($column in $tempcolumn) { + if ($column.length -ne 0) { + $row.item($colnum) = $column + } + else { + $row.item($colnum) = [DBnull]::Value + } + $colnum++ + } + $newrow = $datatable.Rows.Add($row) + } + catch { + Write-Warning "The following line ($i) is causing issues:" + Write-Output $line.Replace($InternalDelimiter, "`n") + + if ($quoted -eq $true) { + Write-Warning "The import has failed, likely because the quoted data was a little too inconsistent. Try using the -Safe parameter." + } + + Write-Verbose "Column datatypes:" + foreach ($c in $datatable.columns) { + Write-Verbose "$($c.columnname) = $($c.datatype)" + } + Write-Error $_.Exception.Message + break + } + } + + if (($i % $batchsize) -eq 0 -or $i -eq $first) { + $bulkcopy.WriteToServer($datatable) + Write-Output "[*] $i rows have been inserted in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + $datatable.Clear() + if ($i -eq $first) { + break + } + } + } + } + # Add in all the remaining rows since the last clear + if ($datatable.Rows.Count -gt 0) { + $bulkcopy.WriteToServer($datatable) + $datatable.Clear() + } + } + else { + # Add rowcount output + $bulkCopy.Add_SqlRowscopied( { + $script:totalrows = $args[1].RowsCopied + if ($resultcount -is [int]) { + $percent = [int](($script:totalrows / $resultcount) * 100) + $timetaken = [math]::Round($elapsed.Elapsed.TotalSeconds, 2) + Write-Progress -id 1 -activity "Inserting $resultcount rows" -percentcomplete $percent -status ([System.String]::Format("Progress: {0} rows ({1}%) in {2} seconds", $script:totalrows, $percent, $timetaken)) + } + else { + Write-Host "$($script:totalrows) rows copied in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + } + }) + + $bulkCopy.WriteToServer($olecmd.ExecuteReader("SequentialAccess")) + if ($resultcount -is [int]) { + Write-Progress -id 1 -activity "Inserting $resultcount rows" -status "Complete" -Completed + } + + } + $completed = $true + } + catch { + # If possible, give more information about common errors. + if ($resultcount -is [int]) { Write-Progress -id 1 -activity "Inserting $resultcount rows" -status "Failed" -Completed } + $errormessage = $_.Exception.Message.ToString() + $completed = $false + if ($errormessage -like "*for one or more required parameters*") { + + Write-Error -Message "Looks like your SQL syntax may be invalid. `nCheck the documentation for more information or start with a simple -Query 'select top 10 * from csv'." + Write-Error -Message "Valid CSV columns are $columns." + + } + elseif ($errormessage -match "invalid column length") { + + # Get more information about malformed CSV input + $pattern = @("\d+") + $match = [regex]::matches($errormessage, @("\d+")) + $index = [int]($match.groups[1].Value) - 1 + $sql = "select name, max_length from sys.columns where object_id = object_id('$table') and column_id = $index" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $datatable = New-Object System.Data.DataTable + $datatable.load($sqlcmd.ExecuteReader()) + $column = $datatable.name + $length = $datatable.max_length + + if ($safe -eq $true) { + Write-Warning "Column $index ($column) contains data with a length greater than $length." + Write-Warning "SqlBulkCopy makes it pretty much impossible to know which row caused the issue, but it's somewhere after row $($script:totalrows)." + } + } + elseif ($errormessage -match "does not allow DBNull" -or $errormessage -match "The given value of type") { + + if ($tablexists -eq $false) { + Write-Error "Looks like the datatype prediction didn't work out. Please create the table manually with proper datatypes then rerun the import script." + } + else { + $sql = "select name from sys.columns where object_id = object_id('$table') order by column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $datatable = New-Object System.Data.DataTable + $datatable.Load($sqlcmd.ExecuteReader()) + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]" }) -join ', ' + Write-Warning "Datatype mismatch." + Write-Output "[*] This is sometimes caused by null handling in SqlBulkCopy, quoted data, or the first row being column names and not data (-FirstRowColumns)." + Write-Output "[*] This could also be because the data types don't match or the order of the columns within the CSV/SQL statement " + Write-Output "[*] do not line up with the order of the table within the SQL Server.`n" + Write-Output "[*] CSV order: $olecolumns`n" + Write-Output "[*] SQL order: $($datatable.rows.name -join ', ')`n" + Write-Output "[*] If this is the case, you can reorder columns by using the -Query parameter or execute the import against a view.`n" + if ($safe -eq $false) { + Write-Output "[*] You can also try running this import using the -Safe parameter, which handles quoted text well.`n" + } + Write-Error "`n$errormessage" + } + + + } + elseif ($errormessage -match "Input string was not in a correct format" -or $errormessage -match "The given ColumnName") { + Write-Warning "CSV contents may be malformed." + Write-Error $errormessage + } + else { Write-Error $errormessage } + } + } + + if ($completed -eq $true) { + # "Note: This count does not take into consideration the number of rows actually inserted when Ignore Duplicates is set to ON." + $null = $transaction.Commit() + + if ($safe -eq $false) { + Write-Output "[*] $i total rows copied" + } + else { + $total = [System.Data.SqlClient.SqlBulkCopyExtension]::RowsCopiedCount($bulkcopy) + Write-Output "[*] $total total rows copied" + } + } + else { + Write-Output "[*] Transaction rolled back." + Write-Output "[*] (Was the proper parameter specified? Is the first row the column name?)." + } + + # Script is finished. Show elapsed time. + $totaltime = [math]::Round($elapsed.Elapsed.TotalSeconds, 2) + Write-Output "[*] Total Elapsed Time for bulk insert: $totaltime seconds" + } + + End { + # Close everything just in case & ignore errors + try { + $null = $sqlconn.close(); $null = $sqlconn.Dispose(); $null = $oleconn.close; + $null = $olecmd.Dispose(); $null = $oleconn.Dispose(); $null = $bulkCopy.close(); + $null = $bulkcopy.dispose(); $null = $reader.close; $null = $reader.dispose() + } + catch { + + } + + # Delete all the temp files + if ($SqlCredentialPath.length -gt 0) { + if ((Test-Path $SqlCredentialPath) -eq $true) { + $null = cmd /c "del $SqlCredentialPath" + } + } + + if ($shellswitch -eq $false -and $safe -eq $true) { + # Delete new schema files + Write-Verbose "Removing automatically generated schema.ini." + foreach ($file in $csv) { + $directory = Split-Path $file + $null = cmd /c "del $directory\schema.ini" | Out-Null + } + + # If a shell switch occured, delete the temporary module file. + if ((Test-Path "$env:TEMP\Import-DbaCsvToSql.psm1") -eq $true) { + cmd /c "del $env:TEMP\Import-DbaCsvToSql.psm1" | Out-Null + } + + # Move original schema.ini's back if they existed + if ($movedschemainis.count -gt 0) { + foreach ($item in $movedschemainis) { + Write-Verbose "Moving $($item.keys) back to $($item.values)." + $null = cmd /c "move $($item.keys) $($item.values)" + } + } + Write-Output "[*] Finished at $(Get-Date)" + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Import-CsvToSql + } +} +function Import-DbaPfDataCollectorSetTemplate { + <# + .SYNOPSIS + Imports a new Performance Monitor Data Collector Set Template either from the dbatools repository or a file you specify. + + .DESCRIPTION + Imports a new Performance Monitor Data Collector Set Template either from the dbatools repository or a file you specify. + When importing data collector sets from the local instance, Run As Admin is required. + + Note: The included counters will be added for all SQL instances on the machine by default. + For specific instances in addition to the default, use -Instance. + + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371952 for more information + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Path + The path to the xml file or files. + + .PARAMETER Template + From one or more of the templates from the dbatools repository. Press Tab to cycle through the available options. + + .PARAMETER RootPath + Sets the base path where the subdirectories are created. + + .PARAMETER DisplayName + Sets the display name of the data collector set. + + .PARAMETER SchedulesEnabled + If this switch is enabled, sets a value that indicates whether the schedules are enabled. + + .PARAMETER Segment + Sets a value that indicates whether PLA creates new logs if the maximum size or segment duration is reached before the data collector set is stopped. + + .PARAMETER SegmentMaxDuration + Sets the duration that the data collector set can run before it begins writing to new log files. + + .PARAMETER SegmentMaxSize + Sets the maximum size of any log file in the data collector set. + + .PARAMETER Subdirectory + Sets a base subdirectory of the root path where the next instance of the data collector set will write its logs. + + .PARAMETER SubdirectoryFormat + Sets flags that describe how to decorate the subdirectory name. PLA appends the decoration to the folder name. For example, if you specify plaMonthDayHour, PLA appends the current month, day, and hour values to the folder name. If the folder name is MyFile, the result could be MyFile110816. + + .PARAMETER SubdirectoryFormatPattern + Sets a format pattern to use when decorating the folder name. Default is 'yyyyMMdd\-NNNNNN'. + + .PARAMETER Task + Sets the name of a Task Scheduler job to start each time the data collector set stops, including between segments. + + .PARAMETER TaskRunAsSelf + If this switch is enabled, sets a value that determines whether the task runs as the data collector set user or as the user specified in the task. + + .PARAMETER TaskArguments + Sets the command-line arguments to pass to the Task Scheduler job specified in the IDataCollectorSet::Task property. + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371992 for more information. + + .PARAMETER TaskUserTextArguments + Sets the command-line arguments that are substituted for the {usertext} substitution variable in the IDataCollectorSet::TaskArguments property. + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371993 for more information. + + .PARAMETER StopOnCompletion + If this switch is enabled, sets a value that determines whether the data collector set stops when all the data collectors in the set are in a completed state. + + .PARAMETER Instance + By default, the template will be applied to all instances. If you want to set specific ones in addition to the default, supply just the instance name. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Import-DbaPfDataCollectorSetTemplate + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' + + Creates a new data collector set named 'Long Running Query' from the dbatools repository on the SQL Server sql2017. + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' -DisplayName 'New Long running query' -Confirm + + Creates a new data collector set named "New Long Running Query" using the 'Long Running Query' template. Forces a confirmation if the template exists. + + .EXAMPLE + Get-DbaPfDataCollectorSet -SqlInstance sql2017 -Session db_ola_health | Remove-DbaData collector set + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template db_ola_health | Start-DbaData collector set + + Imports a session if it exists, then recreates it using a template. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Out-GridView -PassThru | Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 + + Allows you to select a Session template then import to an instance named sql2017. + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' -Instance SHAREPOINT + + Creates a new data collector set named 'Long Running Query' from the dbatools repository on the SQL Server sql2017 for both the default and the SHAREPOINT instance. + + If you'd like to remove counters for the default instance, use Remove-DbaPfDataCollectorCounter. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")] + param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [string]$DisplayName, + [switch]$SchedulesEnabled, + [string]$RootPath, + [switch]$Segment, + [int]$SegmentMaxDuration, + [int]$SegmentMaxSize, + [string]$Subdirectory, + [int]$SubdirectoryFormat = 3, + [string]$SubdirectoryFormatPattern = 'yyyyMMdd\-NNNNNN', + [string]$Task, + [switch]$TaskRunAsSelf, + [string]$TaskArguments, + [string]$TaskUserTextArguments, + [switch]$StopOnCompletion, + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string[]]$Template, + [string[]]$Instance, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\perfmontemplates\collectorsets.xml" + + $setscript = { + $setname = $args[0]; $templatexml = $args[1] + $collectorset = New-Object -ComObject Pla.DataCollectorSet + $collectorset.SetXml($templatexml) + $null = $collectorset.Commit($setname, $null, 0x0003) #add or modify. + $null = $collectorset.Query($setname, $Null) + } + + $instancescript = { + $services = Get-Service -DisplayName *sql* | Select-Object -ExpandProperty DisplayName + [regex]::matches($services, '(?<=\().+?(?=\))').Value | Where-Object { $PSItem -ne 'MSSQLSERVER' } | Select-Object -Unique + } + } + process { + if ((Test-Bound -ParameterName Path -Not) -and (Test-Bound -ParameterName Template -Not)) { + Stop-Function -Message "You must specify Path or Template" + } + + if (($Path.Count -gt 1 -or $Template.Count -gt 1) -and (Test-Bound -ParameterName Template)) { + Stop-Function -Message "Name cannot be specified with multiple files or templates because the Session will already exist" + } + + foreach ($computer in $ComputerName) { + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + foreach ($file in $template) { + $templatepath = "$script:PSModuleRoot\bin\perfmontemplates\collectorsets\$file.xml" + if ((Test-Path $templatepath)) { + $Path += $templatepath + } + else { + Stop-Function -Message "Invalid template ($templatepath does not exist)" -Continue + } + } + + foreach ($file in $Path) { + + if ((Test-Bound -ParameterName DisplayName -Not)) { + Set-Variable -Name DisplayName -Value (Get-ChildItem -Path $file).BaseName + } + + $Name = $DisplayNameUnresolved = $DisplayName + + Write-Message -Level Verbose -Message "Processing $file for $computer" + + if ((Test-Bound -ParameterName RootPath -Not)) { + Set-Variable -Name RootName -Value "%systemdrive%\PerfLogs\Admin\$Name" + } + + # Perform replace + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("").TrimEnd("\") + $tempfile = "$temp\import-dbatools-perftemplate.xml" + + try { + # Get content + $contents = Get-Content $file -ErrorAction Stop + + # Replace content + $replacements = 'RootPath', 'DisplayName', 'SchedulesEnabled', 'Segment', 'SegmentMaxDuration', 'SegmentMaxSize', 'SubdirectoryFormat', 'SubdirectoryFormatPattern', 'Task', 'TaskRunAsSelf', 'TaskArguments', 'TaskUserTextArguments', 'StopOnCompletion', 'DisplayNameUnresolved' + + foreach ($replacement in $replacements) { + $phrase = "<$replacement>" + $value = (Get-Variable -Name $replacement).Value + if ($value -eq $false) { + $value = "0" + } + if ($value -eq $true) { + $value = "1" + } + $replacephrase = "<$replacement>$value" + $contents = $contents.Replace($phrase, $replacephrase) + } + + # Set content + $null = Set-Content -Path $tempfile -Value $contents -Encoding Unicode + $xml = [xml](Get-Content $tempfile -ErrorAction Stop) + $plainxml = Get-Content $tempfile -ErrorAction Stop -Raw + $file = $tempfile + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + if (-not $xml.DataCollectorSet) { + Stop-Function -Message "$file is not a valid Performance Monitor template document" -Continue + } + + try { + Write-Message -Level Verbose -Message "Importing $file as $name " + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command" + + if ($instance) { + $instances = $instance + } + else { + $instances = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $instancescript -ErrorAction Stop -Raw + } + + $scriptblock = { + try { + $results = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $Name, $plainxml -ErrorAction Stop + Write-Message -Level Verbose -Message " $results" + } + catch { + Stop-Function -Message "Failure starting $setname on $computer" -ErrorRecord $_ -Target $computer -Continue + } + } + + if ((Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name)) { + if ($Pscmdlet.ShouldProcess($computer, "CollectorSet $Name already exists. Modify?")) { + Invoke-Command -Scriptblock $scriptblock + $output = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name + } + } + else { + if ($Pscmdlet.ShouldProcess($computer, "Importing collector set $Name")) { + Invoke-Command -Scriptblock $scriptblock + $output = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name + } + } + + $newcollection = @() + foreach ($instance in $instances) { + $datacollector = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name | Get-DbaPfDataCollector + $sqlcounters = $datacollector | Get-DbaPfDataCollectorCounter | Where-Object { $_.Name -match 'sql.*\:' -and $_.Name -notmatch 'sqlclient' } | Select-Object -ExpandProperty Name + + foreach ($counter in $sqlcounters) { + $split = $counter.Split(":") + $firstpart = switch ($split[0]) { + 'SQLServer' { 'MSSQL' } + '\SQLServer' { '\MSSQL' } + default { $split[0] } + } + $secondpart = $split[-1] + $finalcounter = "$firstpart`$$instance`:$secondpart" + $newcollection += $finalcounter + } + } + + if ($newcollection.Count) { + if ($Pscmdlet.ShouldProcess($computer, "Adding $($newcollection.Count) additional counters")) { + $null = Add-DbaPfDataCollectorCounter -InputObject $datacollector -Counter $newcollection + } + } + + Remove-Item $tempfile -ErrorAction SilentlyContinue + $output + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $store -Continue + } + } + } + } +} +function Import-DbaSpConfigure { + <# + .SYNOPSIS + Updates sp_configure settings on destination server. + + .DESCRIPTION + Updates sp_configure settings on destination server. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER SqlInstance + Specifies a SQL Server instance to set up sp_configure values on using a SQL file. + + .PARAMETER SqlCredential + Use this SQL credential if you are setting up sp_configure values from a SQL file. + + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the path to a SQL script file holding sp_configure queries for each of the settings to be changed. Export-DbaSPConfigure creates a suitable file as its output. + + .PARAMETER Force + If this switch is enabled, no version check between Source and Destination is performed. By default, the major and minor versions of Source and Destination must match when copying sp_configure settings. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Import-DbaSpConfigure sqlserver sqlcluster $SourceSqlCredential $DestinationSqlCredential + + Imports the sp_configure settings from the source server sqlserver and sets them on the sqlcluster server + using the SQL credentials stored in the variables + + .EXAMPLE + Import-DbaSpConfigure -SqlInstance sqlserver -Path .\spconfig.sql -SqlCredential $SqlCredential + + Imports the sp_configure settings from the file .\spconfig.sql and sets them on the sqlcluster server + using the SQL credential stored in the variables + + .OUTPUTS + $true if success + $false if failure + + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Parameter(ParameterSetName = "ServerCopy")] + [DbaInstanceParameter]$Source, + [Parameter(ParameterSetName = "ServerCopy")] + [DbaInstanceParameter]$Destination, + [Parameter(ParameterSetName = "ServerCopy")] + [PSCredential]$SourceSqlCredential, + [Parameter(ParameterSetName = "ServerCopy")] + [PSCredential]$DestinationSqlCredential, + [Parameter(ParameterSetName = "FromFile")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Parameter(ParameterSetName = "FromFile")] + [string]$Path, + [Parameter(ParameterSetName = "FromFile")] + [PSCredential]$SqlCredential, + [switch]$Force + + ) + begin { + + if ($Path.length -eq 0) { + $sourceserver = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destserver = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceserver.DomainInstanceName + $destination = $destserver.DomainInstanceName + } + else { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + if ((Test-Path $Path) -eq $false) { + throw "File Not Found" + } + } + + } + process { + if ($Path.length -eq 0) { + if ($Pscmdlet.ShouldProcess($destination, "Export sp_configure")) { + $sqlfilename = Export-SqlSpConfigure $sourceserver + } + + if ($sourceserver.versionMajor -ne $destserver.versionMajor -and $force -eq $false) { + Write-Warning "Source SQL Server major version and Destination SQL Server major version must match for sp_configure migration. Use -Force to override this precaution or check the exported sql file, $sqlfilename, and run manually." + return + } + + If ($Pscmdlet.ShouldProcess($destination, "Execute sp_configure")) { + $sourceserver.Configuration.ShowAdvancedOptions.ConfigValue = $true + $sourceserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + $destserver.Configuration.ShowAdvancedOptions.ConfigValue = $true + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + + $destprops = $destserver.Configuration.Properties + + foreach ($sourceprop in $sourceserver.Configuration.Properties) { + $displayname = $sourceprop.DisplayName + + $destprop = $destprops | where-object { $_.Displayname -eq $displayname } + if ($null -ne $destprop) { + try { + $destprop.configvalue = $sourceprop.configvalue + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + Write-Output "updated $($destprop.displayname) to $($sourceprop.configvalue)." + } + catch { + Write-Error "Could not $($destprop.displayname) to $($sourceprop.configvalue). Feature may not be supported." + } + } + } + try { + $destserver.Configuration.Alter() + } + catch { + $needsrestart = $true + } + + $sourceserver.Configuration.ShowAdvancedOptions.ConfigValue = $false + $sourceserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + $destserver.Configuration.ShowAdvancedOptions.ConfigValue = $false + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + + if ($needsrestart -eq $true) { + Write-Warning "Some configuration options will be updated once SQL Server is restarted." + } + else { + Write-Output "Configuration option has been updated." + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Removing temp file")) { + Remove-Item $sqlfilename -ErrorAction SilentlyContinue + } + + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Importing sp_configure from $Path")) { + $server.Configuration.ShowAdvancedOptions.ConfigValue = $true + $sql = Get-Content $Path + foreach ($line in $sql) { + try { + $server.Query($line) | Out-Null + Write-Output "Successfully executed $line." + } + catch { + Write-Error "$line failed. Feature may not be supported." + } + } + $server.Configuration.ShowAdvancedOptions.ConfigValue = $false + Write-Warning "Some configuration options will be updated once SQL Server is restarted." + } + } + } + end { + if ($Path.length -gt 0) { + $server.ConnectionContext.Disconnect() + } + else { + $sourceserver.ConnectionContext.Disconnect() + $destserver.ConnectionContext.Disconnect() + } + + If ($Pscmdlet.ShouldProcess("console", "Showing finished message")) { + Write-Output "SQL Server configuration options migration finished." + } + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Import-SqlSpConfigure + } +} +function Import-DbaXESessionTemplate { + <# + .SYNOPSIS + Imports a new XESession XML Template + + .DESCRIPTION + Imports a new XESession XML Template either from the dbatools repository or a file you specify. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The Name of the session to create. + + .PARAMETER Path + The path to the xml file or files for the session(s). + + .PARAMETER Template + Specifies the name of one of the templates from the dbatools repository. Press tab to cycle through the provided templates. + + .PARAMETER TargetFilePath + By default, files will be created in the default xel directory. Use TargetFilePath to change all instances of + filename = "file.xel" to filename = "$TargetFilePath\file.xel". Only specify the directory, not the file itself. + + This path is relative to the destination directory + + .PARAMETER TargetFileMetadataPath + By default, files will be created in the default xem directory. Use TargetFileMetadataPath to change all instances of + filename = "file.xem" to filename = "$TargetFilePath\file.xem". Only specify the directory, not the file itself. + + This path is relative to the destination directory + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Import-DbaXESessionTemplate + + .EXAMPLE + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_query_wait_stats + + Creates a new XESession named db_query_wait_stats from the dbatools repository to the SQL Server sql2017. + + .EXAMPLE + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_query_wait_stats -Name "Query Wait Stats" + + Creates a new XESession named "Query Wait Stats" using the db_query_wait_stats template. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2017 -Session db_ola_health | Remove-DbaXESession + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_ola_health | Start-DbaXESession + + Imports a session if it exists, then recreates it using a template. + + .EXAMPLE + Get-DbaXESessionTemplate | Out-GridView -PassThru | Import-DbaXESessionTemplate -SqlInstance sql2017 + + Allows you to select a Session template then import to an instance named sql2017. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Name, + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string[]]$Template, + [string]$TargetFilePath, + [string]$TargetFileMetadataPath, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\xetemplates-metadata.xml" + } + process { + if ((Test-Bound -ParameterName Path -Not) -and (Test-Bound -ParameterName Template -Not)) { + Stop-Function -Message "You must specify Path or Template." + } + + if (($Path.Count -gt 1 -or $Template.Count -gt 1) -and (Test-Bound -ParameterName Template)) { + Stop-Function -Message "Name cannot be specified with multiple files or templates because the Session will already exist." + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + foreach ($file in $template) { + $templatepath = "$script:PSModuleRoot\bin\xetemplates\$file.xml" + if ((Test-Path $templatepath)) { + $Path += $templatepath + } + else { + Stop-Function -Message "Invalid template ($templatepath does not exist)." -Continue + } + } + + foreach ($file in $Path) { + + if ((Test-Bound -Not -ParameterName TargetFilePath)) { + Write-Message -Level Verbose -Message "Importing $file to $instance" + try { + $xml = [xml](Get-Content $file -ErrorAction Stop) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + } + else { + Write-Message -Level Verbose -Message "TargetFilePath specified, changing all file locations in $file for $instance." + Write-Message -Level Verbose -Message "TargetFileMetadataPath specified, changing all metadata file locations in $file for $instance." + + # Handle whatever people specify + $TargetFilePath = $TargetFilePath.TrimEnd("\") + $TargetFileMetadataPath = $TargetFileMetadataPath.TrimEnd("\") + $TargetFilePath = "$TargetFilePath\" + $TargetFileMetadataPath = "$TargetFileMetadataPath\" + + # Perform replace + $xelphrase = 'name="filename" value="' + $xemphrase = 'name="metadatafile" value="' + + try { + $basename = (Get-ChildItem $file).Basename + $contents = Get-Content $file -ErrorAction Stop + $contents = $contents.Replace($xelphrase, "$xelphrase$TargetFilePath") + $contents = $contents.Replace($xemphrase, "$xemphrase$TargetFileMetadataPath") + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("").TrimEnd("\") + $tempfile = "$temp\$basename" + $null = Set-Content -Path $tempfile -Value $contents -Encoding UTF8 + $xml = [xml](Get-Content $tempfile -ErrorAction Stop) + $file = $tempfile + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + Write-Message -Level Verbose -Message "$TargetFilePath does not exist on $server, creating now." + try { + if (-not (Test-DbaSqlPath -SqlInstance $server -Path $TargetFilePath)) { + $null = New-DbaSqlDirectory -SqlInstance $server -Path $TargetFilePath + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + } + + if (-not $xml.event_sessions) { + Stop-Function -Message "$file is not a valid XESession template document." -Continue + } + + if ((Test-Bound -ParameterName Name -not)) { + $Name = (Get-ChildItem $file).BaseName + } + + # This could be done better but not today + $no2012 = ($metadata | Where-Object Compatibility -gt 2012).Name + $no2014 = ($metadata | Where-Object Compatibility -gt 2014).Name + + if ($Name -in $no2012 -and $server.VersionMajor -eq 11) { + Stop-Function -Message "$Name is not supported in SQL Server 2012 ($server)" -Continue + } + + if ($Name -in $no2014 -and $server.VersionMajor -eq 12) { + Stop-Function -Message "$Name is not supported in SQL Server 2014 ($server)" -Continue + } + + if ((Get-DbaXESession -SqlInstance $server -Session $Name)) { + Stop-Function -Message "$Name already exists on $instance" -Continue + } + + try { + Write-Message -Level Verbose -Message "Importing $file as $name " + $session = $store.CreateSessionFromTemplate($Name, $file) + $session.Create() + if ($file -eq $tempfile) { + Remove-Item $tempfile -ErrorAction SilentlyContinue + } + Get-DbaXESession -SqlInstance $server -Session $session.Name + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $store -Continue + } + } + } + } +} +function Install-DbaFirstResponderKit { + <# + .SYNOPSIS + Installs or updates the First Responder Kit stored procedures. + + .DESCRIPTION + Downloads, extracts and installs the First Responder Kit stored procedures: + sp_Blitz, sp_BlitzWho, sp_BlitzFirst, sp_BlitzIndex, sp_BlitzCache and sp_BlitzTrace. + + First Responder Kit links: + http://FirstResponderKit.org + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database to instal the First Responder Kit stored procedures into + + .PARAMETER Branch + Specifies an alternate branch of the First Responder Kit to install. (master or dev) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: BrentOzar, FRK, FirstResponderKit + Author: Tara Kizer, Brent Ozar Unlimited (https://www.brentozar.com/) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Install-DbaFirstResponderKit + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1 -Database master + + Logs into server1 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1\instance1 -Database DBA + + Logs into server1\instance1 with Windows authentication and then installs the FRK in the DBA database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1\instance1 -Database master -SqlCredential $cred + + Logs into server1\instance1 with SQL authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance sql2016\standardrtm, sql2016\sqlexpress, sql2014 + + Logs into sql2016\standardrtm, sql2016\sqlexpress and sql2014 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + $servers = "sql2016\standardrtm", "sql2016\sqlexpress", "sql2014" + $servers | Install-DbaFirstResponderKit + + Logs into sql2016\standardrtm, sql2016\sqlexpress and sql2014 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance sql2016 -Branch dev + + Installs the dev branch version of the FRK in the master database on sql2016 instance. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [ValidateSet('master', 'dev')] + [string]$Branch = "master", + [object]$Database = "master", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $url = "https://codeload.github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/zip/$Branch" + + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\SQL-Server-First-Responder-Kit-$Branch.zip" + $zipfolder = "$temp\SQL-Server-First-Responder-Kit-$Branch\" + + if ($zipfile | Test-Path) { + Remove-Item -Path $zipfile -ErrorAction SilentlyContinue + } + + if ($zipfolder | Test-Path) { + Remove-Item -Path $zipfolder -Recurse -ErrorAction SilentlyContinue + } + + $null = New-Item -ItemType Directory -Path $zipfolder -ErrorAction SilentlyContinue + + Write-Message -Level Verbose -Message "Downloading and unzipping the First Responder Kit zip file." + + try { + $oldSslSettings = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + try { + Invoke-WebRequest $url -OutFile $zipfile + } + catch { + # Try with default proxy and usersettings + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + Invoke-WebRequest $url -OutFile $zipfile + } + [System.Net.ServicePointManager]::SecurityProtocol = $oldSslSettings + + # Unblock if there's a block + Unblock-File $zipfile -ErrorAction SilentlyContinue + + # Unzip the files + $shell = New-Object -ComObject Shell.Application + $zip = $shell.NameSpace($zipfile) + + foreach ($item in $zip.items()) { + $shell.Namespace($temp).CopyHere($item) + } + + Remove-Item -Path $zipfile + } + catch { + Stop-Function -Message "Couldn't download the First Responder Kit. Download and install manually from https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/archive/$Branch.zip." -ErrorRecord $_ + return + } + } + + process { + if (Test-FunctionInterrupt) { + return + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Output -Message "Starting installing/updating the First Responder Kit stored procedures in $database on $instance." + $allprocedures_query = "select name from sys.procedures where is_ms_shipped = 0" + $allprocedures = ($server.Query($allprocedures_query, $Database)).Name + # Install/Update each FRK stored procedure + foreach ($script in (Get-ChildItem $zipfolder -Filter sp_Blitz*.sql)) { + $scriptname = $script.Name + if ($scriptname -ne "sp_BlitzRS.sql") { + $sql = [IO.File]::ReadAllText($script.FullName) + + if ($scriptname -eq "sp_BlitzQueryStore.sql") { + if ($server.VersionMajor -lt 13) { continue } + } + + foreach ($query in ($sql -Split "\nGO\b")) { + $query = $query.Trim() + if ($query) { + try { + $null = $server.Query($query, $Database) + } + catch { + Write-Message -Level Warning -Message "Could not execute at least one portion of $scriptname in $Database on $instance." -ErrorRecord $_ + } + } + } + } + $baseres = @{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $Database + Name = $scriptname.TrimEnd('.sql') + } + if ($scriptname.TrimEnd('.sql') -in $allprocedures) { + $baseres['Status'] = 'Updated' + } + else { + $baseres['Status'] = 'Installed' + } + [PSCustomObject]$baseres + } + Write-Message -Level Output -Message "Finished installing/updating the First Responder Kit stored procedures in $database on $instance." + } + } +} +function Install-DbaMaintenanceSolution { + <# + .SYNOPSIS + Download and Install SQL Server Maintenance Solution created by Ola Hallengren (https://ola.hallengren.com) + .DESCRIPTION + This script will download and install the latest version of SQL Server Maintenance Solution created by Ola Hallengren + + .PARAMETER SqlInstance + The target SQL Server instance onto which the Maintenance Solution will be installed. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database where Ola Hallengren's solution will be installed. Defaults to master. + + .PARAMETER BackupLocation + Location of the backup root directory. If this is not supplied, the default backup directory will be used. + + .PARAMETER CleanupTime + Time in hours, after which backup files are deleted. + + .PARAMETER OutputFileDirectory + Specify the output file directory where the Maintenance Solution will write to. + + .PARAMETER ReplaceExisting + If this switch is enabled, objects already present in the target database will be dropped and recreated. + + .PARAMETER LogToTable + If this switch is enabled, the Maintenance Solution will be configured to log commands to a table. + + .PARAMETER Solution + Specifies which portion of the Maintenance solution to install. Valid values are All (full solution), Backup, IntegrityCheck and IndexOptimize. + + .PARAMETER InstallJobs + If this switch is enabled, the corresponding SQL Agent Jobs will be created. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Ola, Maintenance + Author: Viorel Ciucu, viorel.ciucu@gmail.com, cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + http://dbatools.io/Install-DbaMaintenanceSolution + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -CleanupTime 72 + + Installs Ola Hallengren's Solution objects on RES14224 in the DBA database. + Backups will default to the default Backup Directory. + If the Maintenance Solution already exists, the script will be halted. + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -BackupLocation "Z:\SQLBackup" -CleanupTime 72 + + This will create the Ola Hallengren's Solution objects. Existing objects are not affected in any way. + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -BackupLocation "Z:\SQLBackup" -CleanupTime 72 -ReplaceExisting + + This will drop and then recreate the Ola Hallengren's Solution objects + The cleanup script will drop and recreate: + - TABLE [dbo].[CommandLog] + - STORED PROCEDURE [dbo].[CommandExecute] + - STORED PROCEDURE [dbo].[DatabaseBackup] + - STORED PROCEDURE [dbo].[DatabaseIntegrityCheck] + - STORED PROCEDURE [dbo].[IndexOptimize] + + The following SQL Agent jobs will be deleted: + - 'Output File Cleanup' + - 'IndexOptimize - USER_DATABASES' + - 'sp_delete_backuphistory' + - 'DatabaseBackup - USER_DATABASES - LOG' + - 'DatabaseBackup - SYSTEM_DATABASES - FULL' + - 'DatabaseBackup - USER_DATABASES - FULL' + - 'sp_purge_jobhistory' + - 'DatabaseIntegrityCheck - SYSTEM_DATABASES' + - 'CommandLog Cleanup' + - 'DatabaseIntegrityCheck - USER_DATABASES' + - 'DatabaseBackup - USER_DATABASES - DIFF' + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlServer')] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object]$Database = "master", + [string]$BackupLocation, + [int]$CleanupTime, + [string]$OutputFileDirectory, + [switch]$ReplaceExisting, + [switch]$LogToTable, + [ValidateSet('All', 'Backup', 'IntegrityCheck', 'IndexOptimize')] + [string]$Solution = 'All', + [switch]$InstallJobs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -NonPooled + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ((Test-Bound -Parameter ReplaceExisting -Not)) { + $procs = Get-DbaSqlModule -SqlInstance $server -Database $Database | Where-Object Name -in 'CommandExecute', 'DatabaseBackup', 'DatabaseIntegrityCheck', 'IndexOptimize' + $table = Get-DbaTable -SqlInstance $server -Database $Database -Table CommandLog -IncludeSystemDBs | Where-Object Database -eq $Database + + if ($null -ne $procs -or $null -ne $table) { + Stop-Function -Message "The Maintenance Solution already exists in $Database on $instance. Use -ReplaceExisting to automatically drop and recreate." + return + } + } + + if ((Test-Bound -Parameter BackupLocation -Not)) { + $BackupLocation = (Get-DbaDefaultPath -SqlInstance $server).Backup + } + + Write-Message -Level Output -Message "Ola Hallengren's solution will be installed on database $Database." + + $db = $server.Databases[$Database] + + if ($InstallJobs -and $Solution -ne 'All') { + Stop-Function -Message "To create SQL Agent jobs you need to use '-Solution All' and '-InstallJobs Create'." + return + } + + if ($ReplaceExisting -eq $true) { + Write-Message -Level Verbose -Message "If Ola Hallengren's scripts are found, we will drop and recreate them!" + } + + if ($CleanupTime -ne 0 -and $InstallJobs -eq $false) { + Write-Message -Level Output -Message "CleanupTime $CleanupTime value will be ignored because you chose not to create SQL Agent Jobs." + } + + # Required + $required = @('CommandExecute.sql') + + if ($LogToTable) { + $required += 'CommandLog.sql' + } + + if ($Solution -match 'Backup') { + $required += 'DatabaseBackup.sql' + } + + if ($Solution -match 'IntegrityCheck') { + $required += 'DatabaseIntegrityCheck.sql' + } + + if ($Solution -match 'IndexOptimize') { + $required += 'IndexOptimize.sql' + } + + if ($Solution -match 'All') { + $required += 'MaintenanceSolution.sql' + } + + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\ola.zip" + + # Start the download + $url = "https://github.com/olahallengren/sql-server-maintenance-solution/archive/master.zip" + try { + Start-BitsTransfer -Source $url -DisplayName 'Downloading SQL Server Maintenance Solution - https://ola.hallengren.com' -Destination $zipfile -ErrorAction Stop + } + catch { + Stop-Function -Message "You need to re-run the script, there is a problem with the proxy or the download link has changed." -ErrorRecord $_ + } + + # Unblock if there's a block + Unblock-File $zipfile -ErrorAction SilentlyContinue + + $path = "$temp\sql-server-maintenance-solution-master" + + # We don't like default parameters messed with so we start clean + if ((Test-Path $path)) { + Remove-Item -Path $temp\sql-server-maintenance-solution-master -Recurse -Force -ErrorAction SilentlyContinue + } + + # internal if it doesn't exist + Expand-Archive -Path $zipfile -DestinationPath $temp -Force + Remove-Item -Path $zipfile + + $listOfFiles = Get-ChildItem -Filter "*.sql" -Path $path | Select-Object -ExpandProperty FullName + + # In which database we install + if ($Database -ne 'master') { + $findDB = 'USE [master]' + $replaceDB = 'USE [' + $Database + ']' + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findDB, $replaceDB) | Set-Content -Path $file + } + } + + # Backup location + if ($BackupLocation) { + $findBKP = 'C:\Backup' + $replaceBKP = $BackupLocation + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findBKP, $replaceBKP) | Set-Content -Path $file + } + } + + # CleanupTime + if ($CleanupTime -ne 0) { + $findCleanupTime = 'SET @CleanupTime = NULL' + $replaceCleanupTime = 'SET @CleanupTime = ' + $CleanupTime + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findCleanupTime, $replaceCleanupTime) | Set-Content -Path $file + } + } + + # OutputFileDirectory + if ($OutputFileDirectory.Length -gt 0) { + $findOutputFileDirectory = 'SET @OutputFileDirectory = NULL' + $replaceOutputFileDirectory = 'SET @OutputFileDirectory = N''' + $OutputFileDirectory + '''' + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findOutputFileDirectory, $replaceOutputFileDirectory) | Set-Content -Path $file + } + + } + + # LogToTable + if (!$LogToTable) { + $findLogToTable = "SET @LogToTable = 'Y'" + $replaceLogToTable = "SET @LogToTable = 'N'" + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findLogToTable, $replaceLogToTable) | Set-Content -Path $file + } + } + + # Create Jobs + if ($InstallJobs -eq $false) { + $findCreateJobs = "SET @CreateJobs = 'Y'" + $replaceCreateJobs = "SET @CreateJobs = 'N'" + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findCreateJobs, $replaceCreateJobs) | Set-Content -Path $file + } + } + + $CleanupQuery = $null + if ($ReplaceExisting) { + [string]$CleanupQuery = $(" + IF OBJECT_ID('[dbo].[CommandLog]', 'U') IS NOT NULL + DROP TABLE [dbo].[CommandLog]; + IF OBJECT_ID('[dbo].[CommandExecute]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[CommandExecute]; + IF OBJECT_ID('[dbo].[DatabaseBackup]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[DatabaseBackup]; + IF OBJECT_ID('[dbo].[DatabaseIntegrityCheck]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[DatabaseIntegrityCheck]; + IF OBJECT_ID('[dbo].[IndexOptimize]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[IndexOptimize]; + ") + + Write-Message -Level Output -Message "Dropping objects created by Ola's Maintenance Solution" + $null = $db.Query($CleanupQuery) + + # Remove Ola's Jobs + if ($InstallJobs -and $ReplaceExisting) { + Write-Message -Level Output -Message "Removing existing SQL Agent Jobs created by Ola's Maintenance Solution." + $jobs = Get-DbaAgentJob -SqlInstance $server | Where-Object Description -match "hallengren" + if ($jobs) { + $jobs | ForEach-Object { Remove-DbaAgentJob -SqlInstance $instance -Job $_.name } + } + } + } + + try { + Write-Message -Level Output -Message "Installing on server $SqlInstance, database $Database." + + foreach ($file in $listOfFiles) { + $shortFileName = Split-Path $file -Leaf + if ($required.Contains($shortFileName)) { + Write-Message -Level Output -Message "Installing $file." + $sql = [IO.File]::ReadAllText($file) + try { + foreach ($query in ($sql -Split "\nGO\b")) { + $null = $db.Query($query) + } + } + catch { + Stop-Function -Message "Could not execute $file in $Database on $instance." -ErrorRecord $_ -Target $db -Continue + } + } + } + } + catch { + Stop-Function -Message "Could not execute $file in $Database on $instance." -ErrorRecord $_ -Target $db -Continue + } + } + + if ((Test-Path $path)) { + Remove-Item -Path $temp\sql-server-maintenance-solution-master -Recurse -Force -ErrorAction SilentlyContinue + } + + # Only here due to need for non-pooled connection in this command + try { + $server.ConnectionContext.Disconnect() + } + catch { + } + + Write-Message -Level Output -Message "Installation complete." + } +} +function Install-DbaWatchUpdate { + <# + .SYNOPSIS + Adds the scheduled task to support Watch-DbaUpdate. + + .DESCRIPTION + Adds the scheduled task to support Watch-DbaUpdate. + + .PARAMETER TaskName + Provide custom name for the Scheduled Task + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Module + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Install-DbaWatchUpdate + + .EXAMPLE + Install-DbaWatchUpdate + + Adds the scheduled task needed by Watch-DbaUpdate + + .EXAMPLE + Install-DbaWatchUpdate -TaskName MyScheduledTask + + Will create the scheduled task as the name MyScheduledTask + #> + [cmdletbinding(SupportsShouldProcess)] + param( + [string]$TaskName = 'dbatools version check', + [switch]$EnableException + ) + process { + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Validate Version of OS") ) { + if (([Environment]::OSVersion).Version.Major -lt 10) { + Stop-Function -Message "This command only supports Windows 10 and above" + } + } + $script = { + try { + # create a task, check every 3 hours + $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -NoLogo -NonInteractive -WindowStyle Hidden Watch-DbaUpdate' + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Hours 1) + $principal = New-ScheduledTaskPrincipal -LogonType S4U -UserId (whoami) + $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([timespan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable -DontStopOnIdleEnd + $task = Register-ScheduledTask -Principal $principal -TaskName 'dbatools version check' -Action $action -Trigger $trigger -Settings $settings -ErrorAction Stop + } + catch { + # keep moving + } + } + + if ($null -eq (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue)) { + # Needs admin creds to setup the kind of PowerShell window that doesn't appear for a millisecond + # which is a millisecond too long + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Validate running in RunAs mode")) { + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Message -Level Warning -Message "This command has to run using RunAs mode (privileged) to create the Scheduled Task. This will only happen once." + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Starting process in RunAs mode") ) { + Start-Process powershell -Verb runAs -ArgumentList Install-DbaWatchUpdate -Wait + } + } + + } + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Creating scheduled task $TaskName")) { + try { + Invoke-Command -ScriptBlock $script -ErrorAction Stop + + if ((Get-Location).Path -ne "$env:WINDIR\system32") { + Write-Message -Level Output -Message "Scheduled Task [$TaskName] created! A notification should appear momentarily. Here's something cute to look at in the interim." + Show-Notification -Title "dbatools wants you" -Text "come hang out at dbatools.io/slack" + } + } + catch { + Stop-Function -Message "Could not create scheduled task $TaskName" -Target $env:COMPUTERNAME -ErrorRecord $_ + } + } + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Checking scheduled task was created")) { + # double check + if ($null -eq (Get-ScheduledTask -TaskName "dbatools version check" -ErrorAction SilentlyContinue)) { + Write-Message -Level Warning -Message "Scheduled Task was not created." + } + } + } + else { + Write-Message -Level Output -Message "Scheduled Task $TaskName is already installed on this machine." + } + } +} +function Install-DbaWhoIsActive { + <# + .SYNOPSIS + Automatically installs or updates sp_WhoisActive by Adam Machanic. + + .DESCRIPTION + This command downloads, extracts and installs sp_WhoisActive with Adam's permission. To read more about sp_WhoisActive, please visit http://whoisactive.com and http://sqlblog.com/blogs/adam_machanic/archive/tags/who+is+active/default.aspx + + Please consider donating to Adam if you find this stored procedure helpful: http://tinyurl.com/WhoIsActiveDonate + + Note that you will be prompted a bunch of times to confirm an action. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to install sp_WhoisActive into. This parameter is mandatory when executing this command unattended. + + .PARAMETER LocalFile + Specifies the path to a local file to install sp_WhoisActive from. This can be either the zipfile as distributed by the website or the expanded SQL script. If this parameter is not specified, the latest version will be downloaded and installed from https://whoisactive.com/ + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, the sp_WhoisActive will be downloaded from the internet even if previously cached. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -Database master + + Downloads sp_WhoisActive from the internet and installs to sqlserver2014a's master database. Connects to SQL Server using Windows Authentication. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -SqlCredential $cred + + Pops up a dialog box asking which database on sqlserver2014a you want to install the procedure into. Connects to SQL Server using SQL Authentication. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -Database master -LocalFile c:\SQLAdmin\whoisactive_install.sql + + Installs sp_WhoisActive to sqlserver2014a's master database from the local file whoisactive_install.sql + + .EXAMPLE + $instances = Get-DbaRegisteredServer sqlserver + Install-DbaWhoIsActive -SqlInstance $instances -Database master + + .NOTES + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Install-DbaWhoIsActive + #> + + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $false)] + [ValidateScript( { Test-Path -Path $_ -PathType Leaf })] + [string]$LocalFile, + [object]$Database, + [switch][Alias('Silent')] + $EnableException, + [switch]$Force + ) + + begin { + $DbatoolsData = Get-DbaConfigValue -FullName "Path.DbatoolsData" + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\spwhoisactive.zip" + + if ($LocalFile -eq $null -or $LocalFile.Length -eq 0) { + $baseUrl = "http://whoisactive.com/downloads" + $latest = ((Invoke-WebRequest -UseBasicParsing -uri http://whoisactive.com/downloads).Links | where-object { $PSItem.href -match "who_is_active" } | Select-Object href -First 1).href + $LocalCachedCopy = Join-Path -Path $DbatoolsData -ChildPath $latest; + + if ((Test-Path -Path $LocalCachedCopy -PathType Leaf) -and (-not $Force)) { + Write-Message -Level Verbose -Message "Locally-cached copy exists, skipping download." + if ($PSCmdlet.ShouldProcess($env:computername, "Copying sp_WhoisActive from local cache for installation")) { + Copy-Item -Path $LocalCachedCopy -Destination $zipfile; + } + } + else { + if ($PSCmdlet.ShouldProcess($env:computername, "Downloading sp_WhoisActive")) { + try { + Write-Message -Level Verbose -Message "Downloading sp_WhoisActive zip file, unzipping and installing." + $url = $baseUrl + "/" + $latest + try { + Invoke-WebRequest $url -OutFile $zipfile -ErrorAction Stop -UseBasicParsing + Copy-Item -Path $zipfile -Destination $LocalCachedCopy + } + catch { + #try with default proxy and usersettings + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + Invoke-WebRequest $url -OutFile $zipfile -ErrorAction Stop -UseBasicParsing + } + } + catch { + Stop-Function -Message "Couldn't download sp_WhoisActive. Please download and install manually from $url." -ErrorRecord $_ + return + } + } + } + } + else { + # Look local + if ($PSCmdlet.ShouldProcess($env:computername, "Copying local file to temp directory")) { + + if ($LocalFile.EndsWith("zip")) { + Copy-Item -Path $LocalFile -Destination $zipfile -Force + } + else { + Copy-Item -Path $LocalFile -Destination (Join-Path -path $temp -childpath "whoisactivelocal.sql") + } + } + } + if ($LocalFile -eq $null -or $LocalFile.Length -eq 0 -or $LocalFile.EndsWith("zip")) { + # Unpack + # Unblock if there's a block + if ($PSCmdlet.ShouldProcess($env:computername, "Unpacking zipfile")) { + + Unblock-File $zipfile -ErrorAction SilentlyContinue + + if (Get-Command -ErrorAction SilentlyContinue -Name "Expand-Archive") { + try { + Expand-Archive -Path $zipfile -DestinationPath $temp -Force + } + catch { + Stop-Function -Message "Unable to extract $zipfile. Archive may not be valid." -ErrorRecord $_ + return + } + } + else { + # Keep it backwards compatible + $shell = New-Object -ComObject Shell.Application + $zipPackage = $shell.NameSpace($zipfile) + $destinationFolder = $shell.NameSpace($temp) + Get-ChildItem "$temp\who*active*.sql" | Remove-Item + $destinationFolder.CopyHere($zipPackage.Items()) + } + Remove-Item -Path $zipfile + } + $sqlfile = (Get-ChildItem "$temp\who*active*.sql" -ErrorAction SilentlyContinue | Select-Object -First 1).FullName + } + else { + $sqlfile = $LocalFile + } + + if ($PSCmdlet.ShouldProcess($env:computername, "Reading SQL file into memory")) { + Write-Message -Level Verbose -Message "Using $sqlfile." + + $sql = [IO.File]::ReadAllText($sqlfile) + $sql = $sql -replace 'USE master', '' + $batches = $sql -split "GO\r\n" + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (-not $Database) { + if ($PSCmdlet.ShouldProcess($instance, "Prompting with GUI list of databases")) { + $Database = Show-DbaDatabaseList -SqlInstance $server -Title "Install sp_WhoisActive" -Header "To deploy sp_WhoisActive, select a database or hit cancel to quit." -DefaultDb "master" + + if (-not $Database) { + Stop-Function -Message "You must select a database to install the procedure." -Target $Database + return + } + + if ($Database -ne 'master') { + Write-Message -Level Warning -Message "You have selected a database other than master. When you run Invoke-DbaWhoIsActive in the future, you must specify -Database $Database." + } + } + } + if ($PSCmdlet.ShouldProcess($instance, "Installing sp_WhoisActive")) { + try { + $ProcedureExists_Query = "select COUNT(*) [proc_count] from sys.procedures where is_ms_shipped = 0 and name like '%sp_WhoisActive%'" + + if ($server.Databases[$Database]) { + $ProcedureExists = ($server.Query($ProcedureExists_Query, $Database)).proc_count + foreach ($batch in $batches) { + try { + $null = $server.databases[$Database].ExecuteNonQuery($batch) + } + catch { + Stop-Function -Message "Failed to install stored procedure." -ErrorRecord $_ -Continue -Target $instance + } + } + + if ($ProcedureExists -gt 0) { + $status = 'Updated' + } + else { + $status = 'Installed' + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $Database + Name = 'sp_WhoisActive' + Status = $status + } + } + else { + Stop-Function -Message "Failed to find database $Database on $instance or $Database is not writeable." -ErrorRecord $_ -Continue -Target $instance + } + + } + catch { + Stop-Function -Message "Failed to install stored procedure." -ErrorRecord $_ -Continue -Target $instance + } + + } + } + } + end { + if ($PSCmdlet.ShouldProcess($env:computername, "Post-install cleanup")) { + Get-Item $sqlfile | Remove-Item + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Install-SqlWhoIsActive + } +} +function Invoke-DbaAdvancedRestore { + <# + .SYNOPSIS + Allows the restore of modified BackupHistory Objects + For 90% of users Restore-DbaDatabase should be your point of access to this function. The other 10% use it at their own risk + + .DESCRIPTION + This is the final piece in the Restore-DbaDatabase Stack. Usually a BackupHistory object will arrive here from Restore-DbaDatabse via the following pipeline: + Get-DbaBackupInformation | Select-DbaBackupInformation | Format-DbaBackupInformation | Test-DbaBackupInformation | Invoke-DbaAdvancedRestore + + We have exposed these functions publicly to allow advanced users to perform operations that we don't support, or won't add as they would make things too complex for the majority of our users + + For example if you wanted to do some very complex redirection during a migration, then doing the rewrite of destinations may be better done with your own custom scripts rather than via Format-DbaBackupInformation + + We would recommend ALWAYS pushing your input through Test-DbaBackupInformation just to make sure that it makes sense to us. + + .PARAMETER BackupHistory + The BackupHistory object to be restored. + Can be passed in on the pipeline + + .PARAMETER SqlInstance + The SqlInstance to which the backups should be restored + + .PARAMETER SqlCredential + SqlCredential to be used to connect to the target SqlInstance + + .PARAMETER OutputScriptOnly + If set, the restore will not be performed, but the T-SQL scripts to perform it will be returned + + .PARAMETER VerifyOnly + If set, performs a Verify of the backups rather than a full restore + + .PARAMETER RestoreTime + Point in Time to which the database should be restored. + + This should be the same value or earlier, as used in the previous pipeline stages + + .PARAMETER StandbyDirectory + A folder path where a standby file should be created to put the recovered databases in a standby mode + + .PARAMETER NoRecovery + Leave the database in a restoring state so that further restore may be made + + .PARAMETER MaxTransferSize + Parameter to set the unit of transfer. Values must be a multiple by 64kb + + .PARAMETER Blocksize + Specifies the block size to use. Must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb or 64kb + Can be specified in bytes + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER BufferCount + Number of I/O buffers to use to perform the operation. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER Continue + Indicates that the restore is continuing a restore, so target database must be in Recovering or Standby states + + .PARAMETER AzureCredential + AzureCredential required to connect to blob storage holding the backups + + .PARAMETER WithReplace + Indicated that if the database already exists it should be replaced + + .PARAMETER KeepCDC + Indicates whether CDC information should be restored as part of the database + + .PARAMETER PageRestore + The output from Get-DbaSuspect page containing the suspect pages to be restored. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER EnableException + Replaces user friendly yellow warnings with bloody red exceptions of doom! + Use this if you want the function to throw terminating errors you want to catch. + + .NOTES + Tags: Restore, Backup + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaAdvancedRestore + + .EXAMPLE + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance + + Will restore all the backups in the BackupHistory object according to the transformations it contains + + .EXAMPLE + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance -OutputScriptOnly + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance + + First generates just the T-SQL restore scripts so they can be sanity checked, and then if they are good perform the full restore. By reusing the BackupHistory object there is no need to rescan all the backup files again + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object[]]$BackupHistory, + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$OutputScriptOnly, + [switch]$VerifyOnly, + [datetime]$RestoreTime = (Get-Date).AddDays(2), + [string]$StandbyDirectory, + [switch]$NoRecovery, + [int]$MaxTransferSize, + [int]$BlockSize, + [int]$BufferCount, + [switch]$Continue, + [PSCredential]$AzureCredential, + [switch]$WithReplace, + [switch]$KeepCDC, + [object[]]$PageRestore, + [switch]$EnableException + ) + begin { + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + if ($KeepCDC -and ($NoRecovery -or ('' -ne $StandbyDirectory))) { + Stop-Function -Category InvalidArgument -Message "KeepCDC cannot be specified with Norecovery or Standby as it needs recovery to work" + return + } + + if ($null -ne $PageRestore) { + Write-Message -Message "Doing Page Recovery" -Level Verbose + $tmpPages = @() + foreach ($Page in $PageRestore) { + $tmppages += "$($Page.FileId):$($Page.PageID)" + } + $NoRecovery = $True + $Pages = $tmpPages -join ',' + } + #$OutputScriptOnly = $false + $InternalHistory = @() + } + process { + foreach ($bh in $BackupHistory) { + $InternalHistory += $bh + } + } + end { + if (Test-FunctionInterrupt) { return } + $Databases = $InternalHistory.Database | Select-Object -Unique + foreach ($Database in $Databases) { + if ($Database -in $Server.Databases.Name) { + if (-not $OutputScriptOnly -and -not $VerifyOnly) { + if ($Pscmdlet.ShouldProcess("Killing processes in $Database on $SqlInstance as it exists and WithReplace specified `n", "Cannot proceed if processes exist, ", "Database Exists and WithReplace specified, need to kill processes to restore")) { + try { + Write-Message -Level Verbose -Message "Killing processes on $Database" + $null = Stop-DbaProcess -SqlInstance $Server -Database $Database -WarningAction Silentlycontinue + $null = $server.Query("Alter database $Database set offline with rollback immediate; alter database $Database set restricted_user; Alter database $Database set online with rollback immediate", 'master') + $server.ConnectionContext.Connect() + } + catch { + Write-Message -Level Verbose -Message "No processes to kill in $Database" + } + } + } + elseif (-not $WithReplace -and (-not $VerifyOnly)) { + Stop-Function -Message "$Database exists and WithReplace not specified, stopping" -EnableException $EnableException + return + } + } + Write-Message -Message "WithReplace = $WithReplace" -Level Debug + $backups = @($InternalHistory | Where-Object {$_.Database -eq $Database} | Sort-Object -Property Type, FirstLsn) + $BackupCnt = 1 + foreach ($backup in $backups) { + $Restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + if (($backup -ne $backups[-1]) -or $true -eq $NoRecovery) { + $Restore.NoRecovery = $True + } + elseif ($backup -eq $backups[-1] -and '' -ne $StandbyDirectory) { + $Restore.StandbyFile = $StandByDirectory + "\" + $Database + (get-date -Format yyyMMddHHmmss) + ".bak" + Write-Message -Level Verbose -Message "Setting standby on last file $($Restore.StandbyFile)" + } + else { + $Restore.NoRecovery = $False + } + if ($restoretime -gt (Get-Date) -or $Restore.RestoreTime -gt (Get-Date) -or $backup.RecoveryModel -eq 'Simple') { + $Restore.ToPointInTime = $null + } + else { + if ($RestoreTime -ne $Restore.RestoreTime) { + $Restore.ToPointInTime = $backup.RestoreTime + } + else { + $Restore.ToPointInTime = $RestoreTime + } + } + $Restore.Database = $database + $Restore.ReplaceDatabase = $WithReplace + if ($MaxTransferSize) { + $Restore.MaxTransferSize = $MaxTransferSize + } + if ($BufferCount) { + $Restore.BufferCount = $BufferCount + } + if ($BlockSize) { + $Restore.Blocksize = $BlockSize + } + if ($true -ne $Continue -and ($null -eq $Pages)) { + foreach ($file in $backup.FileList) { + $MoveFile = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile + $MoveFile.LogicalFileName = $File.LogicalName + $MoveFile.PhysicalFileName = $File.PhysicalName + $null = $Restore.RelocateFiles.Add($MoveFile) + } + } + $Action = switch ($backup.Type) { + '1' {'Database'} + '2' {'Log'} + '5' {'Database'} + 'Transaction Log' {'Log'} + Default {'Database'} + } + + Write-Message -Level Debug -Message "restore action = $Action" + $Restore.Action = $Action + foreach ($File in $backup.FullName) { + Write-Message -Message "Adding device $file" -Level Debug + $Device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $Device.Name = $file + if ($file -like "http*") { + $Device.devicetype = "URL" + $Restore.CredentialName = $AzureCredential + } + else { + $Device.devicetype = "File" + } + $Restore.FileNumber = $backup.Position + $Restore.Devices.Add($Device) + } + Write-Message -Level Verbose -Message "Performing restore action" + $ConfirmMessage = "`n Restore Database $Database on $SqlInstance `n from files: $RestoreFileNames `n with these file moves: `n $LogicalFileMovesString `n $ConfirmPointInTime `n" + if ($Pscmdlet.ShouldProcess("$Database on $SqlInstance `n `n", $ConfirmMessage)) { + try { + $RestoreComplete = $true + if ($KeepCDC -and $Restore.NoRecovery -eq $false) { + $script = $Restore.Script($server) + if ($script -like '*WITH*') { + $script = $script.TrimEnd() + ' , KEEP_CDC' + } + else { + $script = $script.TrimEnd() + ' WITH KEEP_CDC' + } + if ($true -ne $OutputScriptOnly) { + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $null = $server.ConnectionContext.ExecuteNonQuery($script) + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + } + } + elseif ($null -ne $Pages -and $Action -eq 'Database') { + $script = $Restore.Script($server) + $script = $script -replace "] FROM", "] PAGE='$pages' FROM" + if ($true -ne $OutputScriptOnly) { + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $null = $server.ConnectionContext.ExecuteNonQuery($script) + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + } + } + elseif ($OutputScriptOnly) { + $script = $Restore.Script($server) + } + elseif ($VerifyOnly) { + Write-Message -Message "VerifyOnly restore" -Level Verbose + Write-Progress -id 2 -activity "Verifying $Database backup file on $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $Verify = $Restore.SqlVerify($server) + Write-Progress -id 2 -activity "Verifying $Database backup file on $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + if ($verify -eq $true) { + Write-Message -Message "VerifyOnly restore Succeeded" -Level Verbose + return "Verify successful" + } + else { + Write-Message -Message "VerifyOnly restore Failed" -Level Verbose + return "Verify failed" + } + } + else { + $outerProgress = $BackupCnt/$Backups.Count*100 + if ($BackupCnt -eq 1) { + Write-Progress -id 2 -ParentId 1 -Activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 + } + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -percentcomplete 0 + $script = $Restore.Script($Server) + $percentcomplete = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $Restore.add_PercentComplete($percentcomplete) + $Restore.PercentCompleteNotification = 1 + $Restore.SqlRestore($Server) + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -Completed + Write-Progress -id 2 -ParentId 1 -Activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete $outerProgress -status ([System.String]::Format("Progress: {0:N2} %", $outerProgress)) + } + } + catch { + Write-Message -Level Verbose -Message "Failed, Closing Server connection" + $RestoreComplete = $False + $ExitError = $_.Exception.InnerException + Stop-Function -Message "Failed to restore db $Database, stopping" -ErrorRecord $_ + return + } + finally { + + if ($OutputScriptOnly -eq $false) { + [PSCustomObject]@{ + SqlInstance = $SqlInstance + DatabaseName = $backup.Database + DatabaseOwner = $server.ConnectionContext.TrueLogin + NoRecovery = $Restore.NoRecovery + WithReplace = $WithReplace + RestoreComplete = $RestoreComplete + BackupFilesCount = $backup.FullName.Count + RestoredFilesCount = $backup.Filelist.PhysicalName.count + BackupSizeMB = if ([bool]($backup.psobject.Properties.Name -contains 'TotalSize')) { [Math]::Round(($backup | Measure-Object -Property TotalSize -Sum).Sum / 1mb, 2) } else { $null } + CompressedBackupSizeMB = if ([bool]($backup.psobject.Properties.Name -contains 'CompressedBackupSize')) { [Math]::Round(($backup | Measure-Object -Property CompressedBackupSize -Sum).Sum / 1mb, 2) } else { $null } + BackupFile = $backup.FullName -Join ',' + RestoredFile = $((Split-Path $backup.FileList.PhysicalName -Leaf) | Sort-Object -Unique) -Join ',' + RestoredFileFull = ($backup.Filelist.PhysicalName -Join ',') + RestoreDirectory = ((Split-Path $backup.FileList.PhysicalName) | Sort-Object -Unique) -Join ',' + BackupSize = if ([bool]($backup.psobject.Properties.Name -contains 'TotalSize')) { ($backup | Measure-Object -Property TotalSize -Sum).Sum } else { $null } + CompressedBackupSize = if ([bool]($backup.psobject.Properties.Name -contains 'CompressedBackupSize')) { ($backup | Measure-Object -Property CompressedBackupSize -Sum).Sum } else { $null } + Script = $script + BackupFileRaw = ($backups.Fullname) + ExitError = $ExitError + } | Select-DefaultView -ExcludeProperty BackupSize, CompressedBackupSize, ExitError, BackupFileRaw, RestoredFileFull + } + else { + $script + } + if ($Restore.Devices.Count -gt 0) { + $Restore.Devices.Clear() + } + Write-Message -Level Verbose -Message "Succeeded, Closing Server connection" + $server.ConnectionContext.Disconnect() + } + } + Write-Progress -id 1 -Activity "Restoring" -Completed + Write-Progress -id 2 -Activity "Restoring" -Completed + $BackupCnt++ + } + if ($server.ConnectionContext.exists) { + $server.ConnectionContext.Disconnect() + } + } + } +} +function Invoke-DbaBalanceDataFiles { + <# + .SYNOPSIS + Re-balance data between data files + + .DESCRIPTION + When you have a large database with a single data file and add another file, SQL Server will only use the new file until it's about the same size. + You may want to balance the data between all the data files. + + The function will check the server version and edition to see if the it allows for online index rebuilds. + If the server does support it, it will try to rebuild the index online. + If the server doesn't support it, it will rebuild the index offline. Be carefull though, this can cause downtime + + The tables must have a clustered index to be able to balance out the data. + The function does NOT yet support heaps. + + The function will also check if the file groups are subject to balance out. + A file group whould have at least have 2 data files and should be writable. + If a table is within such a file group it will be subject for processing. If not the table will be skipped. + + .PARAMETER SqlInstance + The target SQL Server instance or instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. + + .PARAMETER Table + The tables(s) of the database to process. If unspecified, all tables will be processed. + + .PARAMETER RebuildOffline + Will set all the indexes to rebuild offline. + This option is also needed when the server version is below 2005. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + The server does not support online rebuilds of indexes. + Do you want to rebuild the indexes offline? + [Y] Yes [N] No [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + This will disable the check for enough disk space for the action to be successful. + Use this with caution!! + + .NOTES + Tags: Database, FileManagement, File, Space + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 + + This command will distribute the data in database db1 on instance sql1 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 | Select-Object -ExpandProperty DataFilesEnd + + This command will distribute the data in database db1 on instance sql1 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 -Table table1,table2,table5 + + This command will distribute the data for only the tables table1,table2 and table5 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 -RebuildOffline + + This command will consider the fact that there might be a SQL Server edition that does not support online rebuilds of indexes. + By supplying this parameter you give permission to do the rebuilds offline if the edition does not support it. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(ParameterSetName = "Pipe", Mandatory = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [Alias("Tables")] + [object[]]$Table, + [switch]$RebuildOffline, + [switch]$EnableException, + [switch]$Force + ) + + process { + + Write-Message -Message "Starting balancing out data files" -Level Verbose + + # Set the initial success flag + [bool]$success = $true + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Check the database parameter + if ($Database) { + if ($Database -notin $server.Databases.Name) { + Stop-Function -Message "One or more databases cannot be found on instance on instance $instance" -Target $instance -Continue + } + + $DatabaseCollection = $server.Databases | Where-Object { $_.Name -in $Database } + } + else { + Stop-Function -Message "Please supply a database to balance out" -Target $instance -Continue + } + + # Get the server version + $serverVersion = $server.Version.Major + + # Check edition of the sql instance + if ($RebuildOffline) { + Write-Message -Message "Continuing with offline rebuild." -Level Verbose + } + elseif (-not $RebuildOffline -and ($serverVersion -lt 9 -or (([string]$Server.Edition -notmatch "Developer") -and ($Server.Edition -notmatch "Enterprise")))) { + # Set up the confirm part + $message = "The server does not support online rebuilds of indexes. `nDo you want to rebuild the indexes offline?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Set the option to generate a full backup + Write-Message -Message "Continuing with offline rebuild." -Level Verbose + + [bool]$supportOnlineRebuild = $false + } + 1 { + Stop-Function -Message "You chose to not allow offline rebuilds of indexes. Use -RebuildOffline" -Target $instance + return + } + } # switch + } + elseif ($serverVersion -ge 9 -and (([string]$Server.Edition -like "Developer*") -or ($Server.Edition -like "Enterprise*"))) { + [bool]$supportOnlineRebuild = $true + } + + # Loop through each of the databases + foreach ($db in $DatabaseCollection) { + $dataFilesStarting = Get-DbaDatabaseFile -SqlInstance $server -Database $db.Name | Where-Object { $_.TypeDescription -eq 'ROWS' } | Select-Object ID, LogicalName, PhysicalName, Size, UsedSpace, AvailableSpace | Sort-Object ID + + if (-not $Force) { + # Check the amount of disk space available + $query = "SELECT SUBSTRING(physical_name, 0, 4) AS 'Drive' , + SUM(( size * 8 ) / 1024) AS 'SizeMB' + FROM sys.master_files + WHERE DB_NAME(database_id) = '$($db.Name)' + GROUP BY SUBSTRING(physical_name, 0, 4)" + # Execute the query + $dbDiskUsage = $Server.Query($query) + + # Get the free space for each drive + $result = $Server.Query("xp_fixeddrives") + $MbFreeColName = $result[0].psobject.Properties.Name[1] + $diskFreeSpace = $result | Select-Object Drive, @{ Name = 'FreeMB'; Expression = { $_.$MbFreeColName } } + + # Loop through each of the drives to see if the size of files on that + # particular disk do not exceed the free space of that disk + foreach ($d in $dbDiskUsage) { + $freeSpace = $diskFreeSpace | Where-Object { $_.Drive -eq $d.Drive.Trim(':\') } | Select-Object FreeMB + if ($d.SizeMB -gt $freeSpace.FreeMB) { + # Set the success flag + $success = $false + + Stop-Function -Message "The available space may not be sufficient to continue the process. Please use -Force to try anyway." -Target $instance -Continue + return + } + } + } + + # Create the start time + $start = Get-Date + + # Check if the function needs to continue + if ($success) { + + # Get the database files before all the alterations + Write-Message -Message "Retrieving data files before data move" -Level Verbose + Write-Message -Message "Processing database $db" -Level Verbose + + # Check the datafiles of the database + $dataFiles = Get-DbaDatabaseFile -SqlInstance $instance -Database $db | Where-Object { $_.TypeDescription -eq 'ROWS' } + if ($dataFiles.Count -eq 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "Database $db only has one data file. Please add a data file to balance out the data" -Target $instance -Continue + } + + # Check the tables parameter + if ($Table) { + if ($Table -notin $db.Table) { + # Set the success flag + $success = $false + + Stop-Function -Message "One or more tables cannot be found in database $db on instance $instance" -Target $instance -Continue + } + + $TableCollection = $db.Tables | Where-Object { $_.Name -in $Table } + } + else { + $TableCollection = $db.Tables + } + + # Get the database file groups and check the aount of data files + Write-Message -Message "Retrieving file groups" -Level Verbose + $fileGroups = $Server.Databases[$db.Name].FileGroups + + # ARray to hold the file groups with properties + $balanceableTables = @() + + # Loop through each of the file groups + + foreach ($fg in $fileGroups) { + + # If there is less than 2 files balancing out data is not possible + if (($fg.Files.Count -ge 2) -and ($fg.Readonly -eq $false)) { + $balanceableTables += $fg.EnumObjects() | Where-Object { $_.GetType().Name -eq 'Table' } + } + } + + $unsuccessfulTables = @() + + # Loop through each of the tables + foreach ($tbl in $TableCollection) { + + # Chck if the table balanceable + if ($tbl.Name -in $balanceableTables.Name) { + + Write-Message -Message "Processing table $tbl" -Level Verbose + + # Chck the tables and get the clustered indexes + if ($TableCollection.Indexes.Count -lt 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "Table $tbl does not contain any indexes" -Target $instance -Continue + } + else { + + # Get all the clustered indexes for the table + $clusteredIndexes = $TableCollection.Indexes | Where-Object { $_.IndexType -eq 'ClusteredIndex' } + + if ($clusteredIndexes.Count -lt 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "No clustered indexes found in table $tbl" -Target $instance -Continue + } + } + + # Loop through each of the clustered indexes and rebuild them + Write-Message -Message "$($clusteredIndexes.Count) clustered index(es) found for table $tbl" -Level Verbose + if ($PSCmdlet.ShouldProcess("Rebuilding indexes to balance data")) { + foreach ($ci in $clusteredIndexes) { + + Write-Message -Message "Rebuilding index $($ci.Name)" -Level Verbose + + # Get the original index operation + [bool]$originalIndexOperation = $ci.OnlineIndexOperation + + # Set the rebuild option to be either offline or online + if ($RebuildOffline) { + $ci.OnlineIndexOperation = $false + } + elseif ($serverVersion -ge 9 -and $supportOnlineRebuild -and -not $RebuildOffline) { + Write-Message -Message "Setting the index operation for index $($ci.Name) to online" -Level Verbose + $ci.OnlineIndexOperation = $true + } + + # Rebuild the index + try { + Write-Message -Message "Rebuilding index $($ci.Name)" -Level Verbose + $ci.Rebuild() + + # Set the success flag + $success = $true + } + catch { + # Set the original index operation back for the index + $ci.OnlineIndexOperation = $originalIndexOperation + + # Set the success flag + $success = $false + + Stop-Function -Message "Something went wrong rebuilding index $($ci.Name). `n$($_.Exception.Message)" -ErrorRecord $_ -Target $instance -Continue + } + + # Set the original index operation back for the index + Write-Message -Message "Setting the index operation for index $($ci.Name) back to the original value" -Level Verbose + $ci.OnlineIndexOperation = $originalIndexOperation + + } # foreach index + + } # if process + + } # if table is balanceable + else { + # Add the table to the unsuccessful array + $unsuccessfulTables += $tbl.Name + + # Set the success flag + $success = $false + + Write-Message -Message "Table $tbl cannot be balanced out" -Level Verbose + } + + } #foreach table + } + + # Create the end time + $end = Get-Date + + # Create the time span + $timespan = New-TimeSpan -Start $start -End $end + $ts = [timespan]::fromseconds($timespan.TotalSeconds) + $elapsed = "{0:HH:mm:ss}" -f ([datetime]$ts.Ticks) + + # Get the database files after all the alterations + Write-Message -Message "Retrieving data files after data move" -Level Verbose + $dataFilesEnding = Get-DbaDatabaseFile -SqlInstance $server -Database $db.Name | Where-Object { $_.TypeDescription -eq 'ROWS' } | Select-Object ID, LogicalName, PhysicalName, Size, UsedSpace, AvailableSpace | Sort-Object ID + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Start = $start + End = $end + Elapsed = $elapsed + Success = $success + Unsuccessful = $unsuccessfulTables -join "," + DataFilesStart = $dataFilesStarting + DataFilesEnd = $dataFilesEnding + } + + } # foreach database + + } # end process + } +} +function Invoke-DbaCycleErrorLog { + <# + .SYNOPSIS + Cycles the current instance or agent log. + + .DESCRIPTION + Cycles the current error log for the instance (SQL Server) and/or SQL Server Agent. + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Type + The log to cycle. + Accepts: instance or agent. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Log, Cycle + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaCycleLog + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 -Type agent + + Cycles the current error log for the SQL Server Agent on SQL Server instance sql2016 + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 -Type instance + + Cycles the current error log for the SQL Server instance on SQL Server instance sql2016 + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 + + Cycles the current error log for both SQL Server instance and SQL Server Agent on SQL Server instance sql2016 + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateSet('instance', 'agent')] + [string]$Type, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (Test-Bound 'Type') { + if ($Type -notin 'instance', 'agent') { + Stop-Function -Message "The type provided [$Type] for $SqlInstance is not an accepted value. Please use 'Instance' or 'Agent'" + return + } + } + $logToCycle = @() + switch ($Type) { + 'agent' { + $sql = "EXEC msdb.dbo.sp_cycle_agent_errorlog;" + $logToCycle = $Type + } + 'instance' { + $sql = "EXEC master.dbo.sp_cycle_errorlog;" + $logToCycle = $Type + } + default { + $sql = " + EXEC master.dbo.sp_cycle_errorlog; + EXEC msdb.dbo.sp_cycle_agent_errorlog;" + $logToCycle = 'instance', 'agent' + } + } + + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $logs = $logToCycle -join ',' + if ($Pscmdlet.ShouldProcess($server, "Cycle the log(s): $logs")) { + $null = $server.Query($sql) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LogType = $logToCycle + IsSuccessful = $true + Notes = $null + } + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LogType = $logToCycle + IsSuccessful = $false + Notes = $_.Exception + } + Stop-Function -Message "Issue cycling $logs on $server" -Target $server -ErrorRecord $_ -Exception $_.Exception -Continue + } + } + } +} +function Invoke-DbaDatabaseClone { + <# + .SYNOPSIS + Clones a database schema and statistics + + .DESCRIPTION + Clones a database schema and statistics. + + This can be useful for testing query performance without requiring all the space needed for the data in the database. + + Read more at sqlperformance: https://sqlperformance.com/2016/08/sql-statistics/expanding-dbcc-clonedatabase + + Thanks to Microsoft Tiger Team for the code and idea https://github.com/Microsoft/tigertoolbox/ + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to clone - this list is auto-populated from the server. + + .PARAMETER CloneDatabase + The name(s) to clone to. + + .PARAMETER UpdateStatistics + Update the statistics prior to cloning (per Microsoft Tiger Team formula) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Statistics, Performance + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDatabaseClone + + .EXAMPLE + Invoke-DbaDatabaseClone -SqlInstance sql2016 -Database mydb -CloneDatabase myclone + Clones mydb to myclone on sql2016 + + .EXAMPLE + Invoke-DbaDatabaseClone -SqlInstance sql2016 -Database mydb -CloneDatabase myclone, myclone2 -UpdateStatistics + Updates the statistics of mydb then clones to myclone and myclone2 + + #> + [CmdletBinding()] + param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory, ValueFromPipeline)] + [object]$Database, + [string[]]$CloneDatabase, + [switch]$UpdateStatistics, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if (-not $Database.Name -and -not $SqlInstance) { + Stop-Function -Message "You must specify a server name if you did not pipe a database" + } + + $updatestats = "declare @out table(id int identity(1,1),s sysname, o sysname, i sysname, stats_stream varbinary(max), rows bigint, pages bigint) + declare @dbcc table(stats_stream varbinary(max), rows bigint, pages bigint) + declare c cursor for + select object_schema_name(object_id) s, object_name(object_id) o, name i + from sys.indexes + where type_desc in ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE') + declare @s sysname, @o sysname, @i sysname + open c + fetch next from c into @s, @o, @i + while @@FETCH_STATUS = 0 begin + declare @showStats nvarchar(max) = N'DBCC SHOW_STATISTICS(""' + quotename(@s) + '.' + quotename(@o) + '"", ' + quotename(@i) + ') with stats_stream' + insert @dbcc exec sp_executesql @showStats + insert @out select @s, @o, @i, stats_stream, rows, pages from @dbcc + delete @dbcc + fetch next from c into @s, @o, @i + end + close c + deallocate c + + + declare @sql nvarchar(max); + declare @id int; + + select top 1 @id=id,@sql= + 'UPDATE STATISTICS ' + quotename(s) + '.' + quotename(o) + '(' + quotename(i) + + ') with stats_stream = ' + convert(nvarchar(max), stats_stream, 1) + + ', rowcount = ' + convert(nvarchar(max), rows) + ', pagecount = ' + convert(nvarchar(max), pages) + from @out + + WHILE (@@ROWCOUNT <> 0) + BEGIN + exec sp_executesql @sql + delete @out where id = @id + select top 1 @id=id,@sql= + 'UPDATE STATISTICS ' + quotename(s) + '.' + quotename(o) + '(' + quotename(i) + + ') with stats_stream = ' + convert(nvarchar(max), stats_stream, 1) + + ', rowcount = ' + convert(nvarchar(max), rows) + ', pagecount = ' + convert(nvarchar(max), pages) + from @out + END" + + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 12 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $sql2012min = [version]"11.0.7001.0" # SQL 2012 SP4 + $sql2014min = [version]"12.0.5000.0" # SQL 2014 SP2 + $sql2016min = [version]"13.0.4001.0" # SQL 2016 SP1 + + + if ($server.VersionMajor -eq 11 -and $server.Version -lt $sql2012min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2012 SP4 and above required." -Target $server -Continue + } + + if ($server.VersionMajor -eq 12 -and $server.Version -lt $sql2014min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2014 SP2 and above required." -Target $server -Continue + } + + if ($server.VersionMajor -eq 13 -and $server.Version -lt $sql2016min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2016 SP1 and above required." -Target $server -Continue + } + + if (-not $Database.Name) { + [Microsoft.SqlServer.Management.Smo.Database]$database = $server.Databases[$database] + } + + if ($Database.IsSystemObject) { + Stop-Function -Message "Only user databases are supported" -Target $instance -Continue + } + + if (-not $Database.name) { + Stop-Function -Message "Database not found" -Target $instance -Continue + } + + if ($UpdateStatistics) { + try { + Write-Message -Level Verbose -Message "Updating statistics" + $null = $database.Query($updatestats) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + } + + $dbname = $database.Name + + foreach ($db in $CloneDatabase) { + Write-Message -Level Verbose -Message "Cloning $db from $database" + if ($server.Databases[$db]) { + Stop-Function -Message "Destination clone database $db already exists" -Target $instance -Continue + } + else { + try { + $sql = "dbcc clonedatabase('$dbname','$db')" + $null = $database.Query($sql) + $server.Databases.Refresh() + Get-DbaDatabase -SqlInstance $server -Database $db + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + } + } + } + } +} +function Invoke-DbaDatabaseShrink { + <# + .SYNOPSIS + Shrinks all files in a database. This is a command that should rarely be used. + + - Shrinks can cause severe index fragmentation (to the tune of 99%) + - Shrinks can cause massive growth in the database's transaction log + - Shrinks can require a lot of time and system resources to perform data movement + + .DESCRIPTION + Shrinks all files in a database. Databases should be shrunk only when completely necessary. + + Many awesome SQL people have written about why you should not shrink your data files. Paul Randal and Kalen Delaney wrote great posts about this topic: + + http://www.sqlskills.com/blogs/paul/why-you-should-not-shrink-your-data-files + http://sqlmag.com/sql-server/shrinking-data-files + + However, there are some cases where a database will need to be shrunk. In the event that you must shrink your database: + + 1. Ensure you have plenty of space for your T-Log to grow + 2. Understand that shrinks require a lot of CPU and disk resources + 3. Consider running DBCC INDEXDEFRAG or ALTER INDEX ... REORGANIZE after the shrink is complete. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllUserDatabases + Run command against all user databases + + .PARAMETER PercentFreeSpace + Specifies how much to reduce the database in percent, defaults to 0. + + .PARAMETER ShrinkMethod + Specifies the method that is used to shrink the database + Default + Data in pages located at the end of a file is moved to pages earlier in the file. Files are truncated to reflect allocated space. + EmptyFile + Migrates all of the data from the referenced file to other files in the same filegroup. (DataFile and LogFile objects only). + NoTruncate + Data in pages located at the end of a file is moved to pages earlier in the file. + TruncateOnly + Data distribution is not affected. Files are truncated to reflect allocated space, recovering free space at the end of any file. + + .PARAMETER StatementTimeout + Timeout in minutes. Defaults to infinity (shrinks can take a while.) + + .PARAMETER LogsOnly + Deprecated. Use FileType instead + + .PARAMETER FileType + Specifies the files types that will be shrunk + All + All Data and Log files are shrunk, using database shrink (Default) + Data + Just the Data files are shrunk using file shrink + Log + Just the Log files are shrunk using file shrink + + .PARAMETER ExcludeIndexStats + Exclude statistics about fragmentation + + .PARAMETER ExcludeUpdateUsage + Exclude DBCC UPDATE USAGE for database + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Shrink database" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Shrink, Database + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDatabaseShrink + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2016 -Database Northwind,pubs,Adventureworks2014 + + Shrinks Northwind, pubs and Adventureworks2014 to have as little free space as possible. + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2014 -Database AdventureWorks2014 -PercentFreeSpace 50 + + Shrinks AdventureWorks2014 to have 50% free space. So let's say AdventureWorks2014 was 1GB and it's using 100MB space. The database free space would be reduced to 50MB. + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2012 -AllUserDatabases + + Shrinks all databases on SQL2012 (not ideal for production) + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllUserDatabases, + [ValidateRange(0, 99)] + [int]$PercentFreeSpace = 0, + [ValidateSet('Default', 'EmptyFile', 'NoTruncate', 'TruncateOnly')] + [string]$ShrinkMethod = "Default", + [ValidateSet('All', 'Data', 'Log')] + [string]$FileType = "All", + [int]$StatementTimeout = 0, + [switch]$LogsOnly, + [switch]$ExcludeIndexStats, + [switch]$ExcludeUpdateUsage, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($LogsOnly) { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Parameter "LogsOnly" + $FileType = 'Log' + } + + $StatementTimeoutSeconds = $StatementTimeout * 60 + + $sql = "SELECT + indexstats.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats + INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id] + INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id] + INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id] + AND indexstats.index_id = dbindexes.index_id + WHERE indexstats.database_id = DB_ID() AND indexstats.avg_fragmentation_in_percent > 0 + ORDER BY indexstats.avg_fragmentation_in_percent desc" + + $sqlTop1 = "SELECT top 1 + indexstats.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats + INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id] + INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id] + INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id] + AND indexstats.index_id = dbindexes.index_id + WHERE indexstats.database_id = DB_ID() + ORDER BY indexstats.avg_fragmentation_in_percent desc" + } + + process { + if (!$Database -and !$ExcludeDatabase -and !$AllUserDatabases) { + Stop-Function -Message "You must specify databases to execute against using either -Databases, -Exclude or -AllUserDatabases" -Continue + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # changing statement timeout to $StatementTimeout + if ($StatementTimeout -eq 0) { + Write-Message -Level Verbose -Message "Changing statement timeout to infinity" + } + else { + Write-Message -Level Verbose -Message "Changing statement timeout to $StatementTimeout minutes" + } + $server.ConnectionContext.StatementTimeout = $StatementTimeoutSeconds + + $dbs = $server.Databases | Where-Object { $_.IsSystemObject -eq $false -and $_.IsAccessible } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsDatabaseSnapshot) { + Write-Message -Level Warning -Message "The database $db on server $instance is a snapshot and cannot be shrunk. Skipping database." + continue + } + + $startingSize = $db.Size + $spaceAvailableMB = $db.SpaceAvailable / 1024 + $spaceUsed = $startingSize - $spaceAvailableMB + $desiredSpaceAvailable = ($PercentFreeSpace * $spaceUsed) / 100 + + Write-Message -Level Verbose -Message "Starting Size (MB): $startingSize" + Write-Message -Level Verbose -Message "Starting Freespace (MB): $([int]$spaceAvailableMB)" + Write-Message -Level Verbose -Message "Desired Freespace (MB): $([int]$desiredSpaceAvailable)" + + if (($db.SpaceAvailable / 1024) -le $desiredSpaceAvailable) { + Write-Message -Level Warning -Message "Space Available ($spaceAvailableMB) is less than or equal to the desired outcome ($desiredSpaceAvailable)" + } + else { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Shrinking from $([int]$spaceAvailableMB) MB space available to $([int]$desiredSpaceAvailable) MB space available")) { + if ($db.Tables.Indexes.Name -and $server.VersionMajor -gt 8 -and $ExcludeIndexStats -eq $false) { + Write-Message -Level Verbose -Message "Getting average fragmentation" + $startingFrag = ($server.Query($sql, $db.name) | Select-Object -ExpandProperty avg_fragmentation_in_percent | Measure-Object -Average).Average + $startingTopFrag = ($server.Query($sqlTop1, $db.name)).avg_fragmentation_in_percent + } + else { + $startingTopFrag = $startingFrag = $null + } + + $start = Get-Date + + switch ($FileType) { + 'Log' { + try { + Write-Message -Level Verbose -Message "Beginning shrink of log files" + $db.LogFiles.Shrink($desiredSpaceAvailable, $ShrinkMethod) + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + 'Data' { + try { + Write-Message -Level Verbose -Message "Beginning shrink of data files" + foreach ($fileGroup in $db.FileGroups) { + foreach ($file in $fileGroup.Files) { + Write-Message -Level Verbose -Message "Beginning shrink of $($file.Name)" + $file.Shrink($PercentFreeSpace, $ShrinkMethod) + } + } + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + if (-not $ExcludeUpdateUsage) { $db.RecalculateSpaceUsage() } + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + default { + try { + Write-Message -Level Verbose -Message "Beginning shrink of entire database" + $db.Shrink($desiredSpaceAvailable, $ShrinkMethod) + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + if (-not $ExcludeUpdateUsage) { $db.RecalculateSpaceUsage() } + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + } + + $end = Get-Date + $dbSize = $db.Size + $newSpaceAvailableMB = $db.SpaceAvailable / 1024 + + Write-Message -Level Verbose -Message "Final database size: $([int]$dbSize) MB" + Write-Message -Level Verbose -Message "Final space available: $([int]$newSpaceAvailableMB) MB" + + if ($db.Tables.Indexes.Name -and $server.VersionMajor -gt 8 -and $ExcludeIndexStats -eq $false) { + Write-Message -Level Verbose -Message "Refreshing indexes and getting average fragmentation" + $endingDefrag = ($server.Query($sql, $db.name) | Select-Object -ExpandProperty avg_fragmentation_in_percent | Measure-Object -Average).Average + $endingTopDefrag = ($server.Query($sqlTop1, $db.name)).avg_fragmentation_in_percent + } + else { + $endingTopDefrag = $endingDefrag = $null + } + + $timSpan = New-TimeSpan -Start $start -End $end + $ts = [TimeSpan]::fromseconds($timSpan.TotalSeconds) + $elapsed = "{0:HH:mm:ss}" -f ([datetime]$ts.Ticks) + } + } + + #$db.TruncateLog() + + if ($Pscmdlet.ShouldProcess("$db on $instance", "Showing results")) { + if ($null -eq $notes) { + $notes = "Database shrinks can cause massive index fragmentation and negatively impact performance. You should now run DBCC INDEXDEFRAG or ALTER INDEX ... REORGANIZE" + } + $object = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Start = $start + End = $end + Elapsed = $elapsed + Success = $success + StartingTotalSizeMB = [math]::Round($startingSize, 2) + StartingUsedMB = [math]::Round($spaceUsed, 2) + FinalTotalSizeMB = [math]::Round($db.size, 2) + StartingAvailableMB = [math]::Round($spaceAvailableMB, 2) + DesiredAvailableMB = [math]::Round($desiredSpaceAvailable, 2) + FinalAvailableMB = [math]::Round(($db.SpaceAvailable / 1024), 2) + StartingAvgIndexFragmentation = [math]::Round($startingFrag, 1) + EndingAvgIndexFragmentation = [math]::Round($endingDefrag, 1) + StartingTopIndexFragmentation = [math]::Round($startingTopFrag, 1) + EndingTopIndexFragmentation = [math]::Round($endingTopDefrag, 1) + Notes = $notes + } + + if ($ExcludeIndexStats) { + Select-DefaultView -InputObject $object -ExcludeProperty StartingAvgIndexFragmentation, EndingAvgIndexFragmentation, StartingTopIndexFragmentation, EndingTopIndexFragmentation + } + else { + $object + } + } + } + } + } +} + +function Invoke-DbaDatabaseUpgrade { + <# + .SYNOPSIS + Take a database and upgrades it to compatibility of the SQL Instance its hosted on. Based on https://thomaslarock.com/2014/06/upgrading-to-sql-server-2014-a-dozen-things-to-check/ + + .DESCRIPTION + Updates compatibility level, then runs CHECKDB with data_purity, DBCC updateusage, sp_updatestats and finally sp_refreshview against all user views. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is autopopulated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is autopopulated from the server + + .PARAMETER AllUserDatabases + Run command against all user databases + + .PARAMETER Force + Don't skip over databases that are already at the same level the instance is + + .PARAMETER NoCheckDb + Skip checkdb + + .PARAMETER NoUpdateUsage + Skip usage update + + .PARAMETER NoUpdateStats + Skip stats update + + .PARAMETER NoRefreshView + Skip view update + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Update database" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Shrink, Database + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + + .LINK + https://dbatools.io/Invoke-DbaDatabaseUpgrade + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-MSD01 -Database Test + + Runs the below processes against the databases + -- Puts compatibility of database to level of SQL Instance + -- Runs CHECKDB DATA_PURITY + -- Runs DBCC UPDATESUSAGE + -- Updates all users statistics + -- Runs sp_refreshview against every view in the database + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-INT01 -Database Test -NoRefreshView + + Runs the upgrade command skipping the sp_refreshview update on all views + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-INT01 -Database Test -Force + + If database Test is already at the correct compatibility, runs every necessary step + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2016 | Out-GridView -Passthru | Invoke-DbaDatabaseUpgrade + + Get only specific databases using GridView and pass those to Invoke-DbaDatabaseUpgrade +#> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$NoCheckDb, + [switch]$NoUpdateUsage, + [switch]$NoUpdateStats, + [switch]$NoRefreshView, + [switch]$AllUserDatabases, + [switch]$Force, + [parameter(ValueFromPipeline)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + if (Test-Bound -not 'SqlInstance', 'InputObject') { + Write-Message -Level Warning -Message "You must specify either a SQL instance or pipe a database collection" + continue + } + + if (Test-Bound -not 'Database', 'InputObject', 'ExcludeDatabase', 'AllUserDatabases') { + Write-Message -Level Warning -Message "You must explicitly specify a database. Use -Database, -ExcludeDatabase, -AllUserDatabases or pipe a database collection" + continue + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + $server.ConnectionContext.StatementTimeout = [Int32]::MaxValue + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Databases | Where-Object IsAccessible + } + + $InputObject = $InputObject | Where-Object { $_.IsSystemObject -eq $false } + if ($Database) { + $InputObject = $InputObject | Where-Object { $_.Name -contains $Database } + } + if ($ExcludeDatabase) { + $InputObject = $InputObject | Where-Object { $_.Name -notcontains $ExcludeDatabase } + } + + foreach ($db in $InputObject) { + # create objects to use in updates + $server = $db.Parent + $ServerVersion = $server.VersionMajor + Write-Message -Level Verbose -Message "SQL Server is using Version: $ServerVersion" + + $ogcompat = $db.CompatibilityLevel + $dbname = $db.Name + $dbversion = switch ($db.CompatibilityLevel) { + "Version100" { 10 } # SQL Server 2008 + "Version110" { 11 } # SQL Server 2012 + "Version120" { 12 } # SQL Server 2014 + "Version130" { 13 } # SQL Server 2016 + "Version140" { 14 } # SQL Server 2017 + default { 9 } # SQL Server 2005 + } + if (-not $Force) { + # skip over databases at the correct level, unless -Force + if ($dbversion -ge $ServerVersion) { + Write-Message -Level VeryVerbose -Message "Skipping $db because compatibility is at the correct level. Use -Force if you want to run all the additional steps" + continue + } + } + Write-Message -Level Verbose -Message "Updating $db compatibility to SQL Instance level" + if ($dbversion -lt $ServerVersion) { + If ($Pscmdlet.ShouldProcess($server, "Updating $db version on $server from $dbversion to $ServerVersion")) { + $Comp = $ServerVersion * 10 + $tsqlComp = "ALTER DATABASE $db SET COMPATIBILITY_LEVEL = $Comp" + try { + $db.ExecuteNonQuery($tsqlComp) + $comResult = $Comp + } + catch { + Write-Message -Level Warning -Message "Failed run Compatibility Upgrade" -ErrorRecord $_ -Target $instance + $comResult = "Fail" + } + } + } + else { + $comResult = "No change" + } + + if (!($NoCheckDb)) { + Write-Message -Level Verbose -Message "Updating $db with DBCC CHECKDB DATA_PURITY" + If ($Pscmdlet.ShouldProcess($server, "Updating $db with DBCC CHECKDB DATA_PURITY")) { + $tsqlCheckDB = "DBCC CHECKDB ('$dbname') WITH DATA_PURITY, NO_INFOMSGS" + try { + $db.ExecuteNonQuery($tsqlCheckDB) + $DataPurityResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed run DBCC CHECKDB with DATA_PURITY on $db" -ErrorRecord $_ -Target $instance + $DataPurityResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignoring CHECKDB DATA_PURITY" + } + + if (!($NoUpdateUsage)) { + Write-Message -Level Verbose -Message "Updating $db with DBCC UPDATEUSAGE" + If ($Pscmdlet.ShouldProcess($server, "Updating $db with DBCC UPDATEUSAGE")) { + $tsqlUpdateUsage = "DBCC UPDATEUSAGE ($db) WITH NO_INFOMSGS;" + try { + $db.ExecuteNonQuery($tsqlUpdateUsage) + $UpdateUsageResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed to run DBCC UPDATEUSAGE on $db" -ErrorRecord $_ -Target $instance + $UpdateUsageResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignore DBCC UPDATEUSAGE" + $UpdateUsageResult = "Skipped" + } + + if (!($NoUpdatestats)) { + Write-Message -Level Verbose -Message "Updating $db statistics" + If ($Pscmdlet.ShouldProcess($server, "Updating $db statistics")) { + $tsqlStats = "EXEC sp_updatestats;" + try { + $db.ExecuteNonQuery($tsqlStats) + $UpdateStatsResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed to run sp_updatestats on $db" -ErrorRecord $_ -Target $instance + $UpdateStatsResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignoring sp_updatestats" + $UpdateStatsResult = "Skipped" + } + + if (!($NoRefreshView)) { + Write-Message -Level Verbose -Message "Refreshing $db Views" + $dbViews = $db.Views | Where-Object IsSystemObject -eq $false + $RefreshViewResult = "Success" + foreach ($dbview in $dbviews) { + $viewName = $dbView.Name + $viewSchema = $dbView.Schema + $fullName = $viewSchema + "." + $viewName + + $tsqlupdateView = "EXECUTE sp_refreshview N'$fullName'; " + + If ($Pscmdlet.ShouldProcess($server, "Refreshing view $fullName on $db")) { + try { + $db.ExecuteNonQuery($tsqlupdateView) + } + catch { + Write-Message -Level Warning -Message "Failed update view $fullName on $db" -ErrorRecord $_ -Target $instance + $RefreshViewResult = "Fail" + } + } + } + } + else { + Write-Message -Level Verbose -Message "Ignore View Refreshes" + $RefreshViewResult = "Skipped" + } + + If ($Pscmdlet.ShouldProcess("console", "Outputting object")) { + $db.Refresh() + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OriginalCompatibility = $ogcompat.ToString().Replace('Version', '') + CurrentCompatibility = $db.CompatibilityLevel.ToString().Replace('Version', '') + Compatibility = $comResult + DataPurity = $DataPurityResult + UpdateUsage = $UpdateUsageResult + UpdateStats = $UpdateStatsResult + RefreshViews = $RefreshViewResult + } + } + } + } +} +function Invoke-DbaDbDecryptObject { + <# + .SYNOPSIS + Invoke-DbaDbDecryptObject returns the decrypted version of an object + + .DESCRIPTION + When a procedure or a function is created with encryption and you lost the code you're in trouble. + You cannot alter the object or view the definition. + With this command you can search for the object and decrypt the it. + + The command will output the results to the console. + There is an option to export all the results to a folder creating .sql files. + + Make sure the instance allowed dedicated administrator connections (DAC). + The binary versions of the objects can only be retrieved using a DAC connection. + You can check the DAC connection with: + 'Get-DbaSpConfigure -SqlInstance [yourinstance] -ConfigName RemoteDacConnectionsEnabled' + It should say 1 in the ConfiguredValue. + + To change the configurations you can use the Set-DbaSpConfigure command: + 'Set-DbaSpConfigure -SqlInstance [yourinstance] -ConfigName RemoteDacConnectionsEnabled -Value 1' + In some cases you may need to reboot the instance. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Database to look through for the object. + + .PARAMETER ObjectName + The name of the object to search for in the database. + + .PARAMETER EncodingType + The encoding that's used to decrypt and encrypt values. + + .PARAMETER ExportDestination + Used for exporting the results to. + The destiation will use the instance name, database name and object type i.e.: C:\temp\decrypt\SQLDB1\DB1\StoredProcedure + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Encryption, Decrypt, Database + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDbDecryptObject + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1 + + Decrypt object "Function1" in DB1 of instance SQLDB1 and output the data to the user. + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1 -ExportDestination C:\temp\decrypt + + Decrypt object "Function1" in DB1 of instance SQLDB1 and output the data to the folder "C:\temp\decrypt". + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ExportDestination C:\temp\decrypt + + Decrypt all objects in DB1 of instance SQLDB1 and output the data to the folder "C:\temp\decrypt" + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1, Function2 + + Decrypt objects "Function1" and "Function2" and output the data to the user. + + .EXAMPLE + "SQLDB1" | Invoke-DbaDbDecryptObject -Database DB1 -ObjectName Function1, Function2 + + Decrypt objects "Function1" and "Function2" and output the data to the user using a pipeline for the instance. + #> + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [object[]]$Database, + [string[]]$ObjectName, + [ValidateSet('ASCII', 'UTF8')] + [string]$EncodingType = 'ASCII', + [string]$ExportDestination, + [switch]$EnableException + ) + + begin { + + function Invoke-DecryptData() { + param( + [parameter(Mandatory = $true)] + [byte[]]$Secret, + [parameter(Mandatory = $true)] + [byte[]]$KnownPlain, + [parameter(Mandatory = $true)] + [byte[]]$KnownSecret + ) + + # Declare pointers + [int]$i = 0 + + # Loop through each of the characters and apply an XOR to decrypt the data + $result = $( + + # Loop through the byte string + while ($i -lt $Secret.Length) { + + # Compare the byte string character to the key character using XOR + if ($i -lt $Secret.Length) { + $Secret[$i] -bxor $KnownPlain[$i] -bxor $KnownSecret[$i] + } + + # Increment the byte string indicator + $i += 2 + + } # end while loop + + ) # end data value + + # Get the string value from the data + $decryptedData = $Encoding.GetString($result) + + # Return the decrypted data + return $decryptedData + } + + # Create array list to hold the results + $objectCollection = New-Object System.Collections.ArrayList + + # Set the encoding + if ($EncodingType -eq 'ASCII') { + $encoding = [System.Text.Encoding]::ASCII + } + elseif ($EncodingType -eq 'UTF8') { + $encoding = [System.Text.Encoding]::UTF8 + } + + # Check the export parameter + if ($ExportDestination -and -not (Test-Path $ExportDestination)) { + try { + # Create the new destination + New-Item -Path $ExportDestination -ItemType Directory -Force | Out-Null + } + catch { + Stop-Function -Message "Couldn't create destination folder $ExportDestination" -ErrorRecord $_ -Target $instance -Continue + } + } + + } + + process { + + if (Test-FunctionInterrupt) { return } + + # Loop through all the instances + foreach ($instance in $SqlInstance) { + + # Check the configuration of the intance to see if the DAC is enabled + $config = Get-DbaSpConfigure -SqlInstance $instance -ConfigName RemoteDacConnectionsEnabled + if ($config.ConfiguredValue -ne 1) { + Stop-Function -Message "DAC is not enabled for instance $instance.`nPlease use 'Set-DbaSpConfigure -SqlInstance $instance -ConfigName RemoteDacConnectionsEnabled -Value 1' to configure the instance to allow DAC connections" -Target $instance -Continue + } + + # Try to connect to instance + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = New-Object Microsoft.SqlServer.Management.Smo.Server "ADMIN:$instance" + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Get all the databases that compare to the database parameter + $databaseCollection = $server.Databases | Where-Object {$_.Name -in $Database} + + # Loop through each of databases + foreach ($db in $databaseCollection) { + # Get the objects + if($ObjectName){ + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + } + else{ + # Get all encrypted objects + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + } + + <# Get all the objects + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + #> + + # Check if there are any objects + if ($storedProcedures.Count -ge 1) { + $objectCollection += $storedProcedures + } + if ($functions.Count -ge 1) { + $objectCollection += $functions + } + if ($views.Count -ge 1) { + $objectCollection += $views + } + + # Loop through all the objects + foreach ($object in $objectCollection) { + + # Setup the query to get the secret + $querySecret = "SELECT imageval AS Value FROM sys.sysobjvalues WHERE objid = OBJECT_ID('$($object.Name)')" + + # Get the result of the secret query + try { + $secret = $server.Databases[$db.Name].Query($querySecret) + } + catch { + Stop-Function -Message "Couldn't retrieve secret from $instance" -ErrorRecord $_ -Target $instance -Continue + } + + # Check if at least a value came back + if ($secret) { + + # Setup a known plain command and get the binary version of it + switch ($object.ObjectType) { + + 'StoredProcedure' { + $queryKnownPlain = (" " * $secret.Value.Length) + "ALTER PROCEDURE $($object.Schema).$($object.Name) WITH ENCRYPTION AS RETURN 0;" + } + 'UserDefinedFunction' { + + switch ($object.SubType) { + 'Inline' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS TABLE WITH ENCRYPTION AS BEGIN RETURN SELECT 0 i END;" + } + 'Scalar' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS INT WITH ENCRYPTION AS BEGIN RETURN 0 END;" + } + 'Table' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS @r TABLE(i INT) WITH ENCRYPTION AS BEGIN RETURN END;" + } + } + } + 'View' { + $queryKnownPlain = (" " * $secret.Value.Length) + "ALTER VIEW $($object.Schema).$($object.Name) WITH ENCRYPTION AS SELECT NULL AS [Value];" + } + } + + # Convert the known plain into binary + if ($queryKnownPlain) { + try { + $knownPlain = $encoding.GetBytes(($queryKnownPlain)) + } + catch { + Stop-Function -Message "Couldn't convert the known plain to binary" -ErrorRecord $_ -Target $instance -Continue + } + } + else { + Stop-Function -Message "Something went wrong setting up the known plain" -ErrorRecord $_ -Target $instance -Continue + } + + # Setup the query to change the object in SQL Server and roll it back getting the encrypted version + $queryKnownSecret = " + BEGIN TRANSACTION; + EXEC ('$queryKnownPlain'); + SELECT imageval AS Value + FROM sys.sysobjvalues + WHERE objid = OBJECT_ID('$($object.Name)'); + ROLLBACK; + " + + # Get the result for the known encrypted + try { + $knownSecret = $server.Databases[$db.Name].Query($queryKnownSecret) + } + catch { + Stop-Function -Message "Couldn't retrieve known secret from $instance" -ErrorRecord $_ -Target $instance -Continue + } + + # Get the result + $result = Invoke-DecryptData -Secret $secret.value -KnownPlain $knownPlain -KnownSecret $knownSecret.value + + # Check if the results need to be exported + if ($ExportDestination) { + # make up the file name + $filename = "$($object.Schema).$($object.Name).sql" + + # Check the export destination + if ($ExportDestination.EndsWith("\")) { + $destinationFolder = "$ExportDestination$instance\$($db.Name)\$($object.ObjectType)\" + } + else { + $destinationFolder = "$ExportDestination\$instance\$($db.Name)\$($object.ObjectType)\" + } + + # Check if the destination folder exists + if (-not (Test-Path $destinationFolder)) { + try { + # Create the new destination + New-Item -Path $destinationFolder -ItemType Directory -Force:$Force | Out-Null + } + catch { + Stop-Function -Message "Couldn't create destination folder $destinationFolder" -ErrorRecord $_ -Target $instance -Continue + } + } + + # Combine the destination folder and the file name to get the path + $filePath = $destinationFolder + $filename + + # Export the result + try { + $result | Out-File -FilePath $filePath -Force + } + catch { + Stop-Function -Message "Couldn't export the results of $($object.Name) to $filePath" -ErrorRecord $_ -Target $instance -Continue + } + + } + + # Add the results to the custom object + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Type = $object.ObjectType + Schema = $object.Schema + Name = $object.Name + FullName = "$($object.Schema).$($object.Name)" + Script = $result + } + + } # end if secret + + } # end for each object + + } # end for each database + + } # end for each instance + + } # process + + end { + if (Test-FunctionInterrupt) { return } + + Write-Message -Message "Finished decrypting data" -Level Verbose + } +} +function Invoke-DbaDiagnosticQuery { + <# + .SYNOPSIS + Invoke-DbaDiagnosticQuery runs the scripts provided by Glenn Berry's DMV scripts on specified servers + + .DESCRIPTION + This is the main function of the Sql Server Diagnostic Queries related functions in dbatools. + The diagnostic queries are developed and maintained by Glenn Berry and they can be found here along with a lot of documentation: + http://www.sqlskills.com/blogs/glenn/category/dmv-queries/ + + The most recent version of the diagnostic queries are included in the dbatools module. + But it is possible to download a newer set or a specific version to an alternative location and parse and run those scripts. + It will run all or a selection of those scripts on one or multiple servers and return the result as a PowerShell Object + + .PARAMETER SqlInstance + The target SQL Server. Can be either a string or SMO server + + .PARAMETER SqlCredential + Allows alternative Windows or SQL login credentials to be used + + .PARAMETER Path + Alternate path for the diagnostic scripts + + .PARAMETER Database + The database(s) to process. If unspecified, all databases will be processed + + .PARAMETER ExcludeDatabase + The database(s) to exclude + + .PARAMETER UseSelectionHelper + Provides a gridview with all the queries to choose from and will run the selection made by the user on the Sql Server instance specified. + + .PARAMETER QueryName + Only run specific query + + .PARAMETER InstanceOnly + Run only instance level queries + + .PARAMETER DatabaseSpecific + Run only database level queries + + .PARAMETER NoQueryTextColumn + Use this switch to exclude the [Complete Query Text] column from relevant queries + + .PARAMETER NoPlanColumn + Use this switch to exclude the [Query Plan] column from relevant queries + + .PARAMETER NoColumnParsing + Does not parse the [Complete Query Text] and [Query Plan] columns and disregards the NoQueryTextColumn and NoColumnParsing switches + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts to confirm certain actions + + .PARAMETER WhatIf + Shows what would happen if the command would execute, but does not actually perform the command + + .PARAMETER OutputPath + Directory to parsed diagnostict queries to. This will split them based on server, databasename, and query. + + .PARAMETER ExportQueries + Use this switch to export the diagnostic queries to sql files. I + nstead of running the queries, the server will be evaluated to find the appropriate queries to run based on SQL Version. + These sql files will then be created in the OutputDirectory. + + + + .NOTES + Tags: Database, DMV + Author: André Kamman (@AndreKamman), http://clouddba.io + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDiagnosticQuery + + .EXAMPLE + Invoke-DbaDiagnosticQuery -SqlInstance sql2016 + + Run the selection made by the user on the Sql Server instance specified. + + .EXAMPLE + Invoke-DbaDiagnosticQuery -SqlInstance sql2016 -UseSelectionHelper | Export-DbaDiagnosticQuery -Path C:\temp\gboutput + + Provides a gridview with all the queries to choose from and will run the selection made by the user on the SQL Server instance specified. + Then it will export the results to Export-DbaDiagnosticQuery. + + .Example + # Exporting Queries To SQL Files + - Parse the appropriate diagnostic queries by connecting to server to get version matched queries + - Instead of running the diagnostic queries, export them to SQL files + + # Export All Queries to Disk + Invoke-DbaDiagnosticQuery -sqlinstance localhost -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries for all User Dbs + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries For One Target Database + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries For One Target Database and One Specific Query + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" -queryname 'Database-scoped Configurations' + + # Choose Queries To Export + Invoke-DbaDiagnosticQuery -sqlinstance localhost -UseSelectionHelper + + This will export with SqlInstance, DatabaseName, and QueryName as appropriate based on query. + + .Example + # Export Queries (not run) as returned object + + [System.Management.Automation.PSObject[]]$results = Invoke-DbaDiagnosticQuery -SqlInstance localhost -whatif + + Parse the appropriate diagnostic queries by connecting to server, and instead of running them, return as [pscustomobject[]] to work with further + + .Example + # Database Specific Handling + $results = Invoke-DbaDiagnosticQuery -SqlInstance $script:instance2 -DatabaseSpecific -queryname 'Database-scoped Configurations' -databasename $database + + Run diagnostic queries targeted at specific database, and only run database level queries against this database. + + #> + + [CmdletBinding(SupportsShouldProcess)] + [outputtype([pscustomobject[]])] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + + [Alias('DatabaseName')] + [object[]]$Database, + + [object[]]$ExcludeDatabase, + + [Alias('Credential')] + [PSCredential]$SqlCredential, + [System.IO.FileInfo]$Path, + [string[]]$QueryName, + [switch]$UseSelectionHelper, + [switch]$InstanceOnly, + [switch]$DatabaseSpecific, + [Switch]$NoQueryTextColumn, + [Switch]$NoPlanColumn, + [Switch]$NoColumnParsing, + + [string]$OutputPath, + [switch]$ExportQueries, + + [switch][Alias('Silent')] + [switch]$EnableException + ) + + begin { + $ProgressId = Get-Random + + function Invoke-DiagnosticQuerySelectionHelper { + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true)] + $ParsedScript + ) + + $ParsedScript | Select-Object QueryNr, QueryName, DBSpecific, Description | Out-GridView -Title "Diagnostic Query Overview" -OutputMode Multiple | Sort-Object QueryNr | Select-Object -ExpandProperty QueryName + + } + + Write-Message -Level Verbose -Message "Interpreting DMV Script Collections" + + $module = Get-Module -Name dbatools + $base = $module.ModuleBase + + if (!$Path) { + $Path = "$base\bin\diagnosticquery" + } + + $scriptversions = @() + $scriptfiles = Get-ChildItem "$Path\SQLServerDiagnosticQueries_*_*.sql" + + if (!$scriptfiles) { + Write-Message -Level Warning -Message "Diagnostic scripts not found in $Path. Using the ones within the module." + + $Path = "$base\bin\diagnosticquery" + + $scriptfiles = Get-ChildItem "$base\bin\diagnosticquery\SQLServerDiagnosticQueries_*_*.sql" + if (!$scriptfiles) { + Stop-Function -Message "Unable to download scripts, do you have an internet connection? $_" -ErrorRecord $_ + return + } + } + + [int[]]$filesort = $null + + foreach ($file in $scriptfiles) { + $filesort += $file.BaseName.Split("_")[2] + } + + $currentdate = $filesort | Sort-Object -Descending | Select-Object -First 1 + + foreach ($file in $scriptfiles) { + if ($file.BaseName.Split("_")[2] -eq $currentdate) { + $parsedscript = Invoke-DbaDiagnosticQueryScriptParser -filename $file.fullname -NoQueryTextColumn:$NoQueryTextColumn -NoPlanColumn:$NoPlanColumn -NoColumnParsing:$NoColumnParsing + + $newscript = [pscustomobject]@{ + Version = $file.Basename.Split("_")[1] + Script = $parsedscript + } + $scriptversions += $newscript + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $counter = 0 + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Verbose -Message "Collecting diagnostic query data from server: $instance" + + if ($server.VersionMinor -eq 50) { + $version = "2008R2" + } + else { + $version = switch ($server.VersionMajor) { + 9 { "2005" } + 10 { "2008" } + 11 { "2012" } + 12 { "2014" } + 13 { "2016" } + 14 { "2017" } + } + } + + if (!$instanceOnly) { + if (-not $Database) { + $databases = (Get-DbaDatabase -SqlInstance $server -ExcludeAllSystemDb -ExcludeDatabase $ExcludeDatabase).Name + } + else { + $databases = (Get-DbaDatabase -SqlInstance $server -ExcludeAllSystemDb -Database $Database -ExcludeDatabase $ExcludeDatabase).Name + } + } + + $parsedscript = $scriptversions | Where-Object -Property Version -eq $version | Select-Object -ExpandProperty Script + + if ($null -eq $first) { $first = $true } + if ($UseSelectionHelper -and $first) { + $QueryName = Invoke-DiagnosticQuerySelectionHelper $parsedscript + $first = $false + } + #since some database level queries can take longer (such as fragmentation) calculate progress with database specific queries * count of databases to run against into context + $CountOfDatabases = ($databases).Count + + + if ($QueryName.Count -ne 0) { + #if running all queries, then calculate total to run by instance queries count + (db specific count * databases to run each against) + $countDBSpecific = @($parsedscript | Where-Object {$_.QueryName -in $QueryName -and $_.DBSpecific -eq $true}).Count + $countInstanceSpecific = @($parsedscript | Where-Object {$_.QueryName -in $QueryName -and $_.DBSpecific -eq $false}).Count + } + else { + #if narrowing queries to database specific, calculate total to process based on instance queries count + (db specific count * databases to run each against) + $countDBSpecific = @($parsedscript | Where-Object DBSpecific).Count + $countInstanceSpecific = @($parsedscript | Where-Object DBSpecific -eq $false).Count + + } + if (!$instanceonly -and !$DatabaseSpecific -and !$QueryName) { + $scriptcount = $countInstanceSpecific + ($countDBSpecific * $CountOfDatabases ) + } + elseif ($instanceOnly) { + $scriptcount = $countInstanceSpecific + } + elseif ($DatabaseSpecific) { + $scriptcount = $countDBSpecific * $CountOfDatabases + } + elseif ($QueryName.Count -ne 0) { + $scriptcount = $countInstanceSpecific + ($countDBSpecific * $CountOfDatabases ) + + + } + + foreach ($scriptpart in $parsedscript) { + + if (($QueryName.Count -ne 0) -and ($QueryName -notcontains $scriptpart.QueryName)) { continue } + if (!$scriptpart.DBSpecific -and !$DatabaseSpecific) { + if ($ExportQueries) { + $null = New-Item -Path $OutputPath -ItemType Directory -Force + $FileName = Remove-InvalidFileNameChars ('{0}.sql' -f $Scriptpart.QueryName) + $FullName = Join-Path $OutputPath $FileName + Write-Message -Level Verbose -Message "Creating file: $FullName" + $scriptPart.Text | out-file -FilePath $FullName -Encoding UTF8 -force + continue + } + + if ($PSCmdlet.ShouldProcess($instance, $scriptpart.QueryName)) { + + if (-not $EnableException) { + $Counter++ + Write-Progress -Id $ProgressId -ParentId 0 -Activity "Collecting diagnostic query data from $instance" -Status "Processing $counter of $scriptcount" -CurrentOperation $scriptpart.QueryName -PercentComplete (($counter / $scriptcount) * 100) + } + + try { + $result = $server.Query($scriptpart.Text) + Write-Message -Level Verbose -Message "Processed $($scriptpart.QueryName) on $instance" + if (!$result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "Empty Result for this Query" + Result = $null + } + Write-Message -Level Verbose -Message ("Empty result for Query {0} - {1} - {2}" -f $scriptpart.QueryNr, $scriptpart.QueryName, $scriptpart.Description) + } + } + catch { + Write-Message -Level Verbose -Message ('Some error has occured on Server: {0} - Script: {1}, result unavailable' -f $instance, $scriptpart.QueryName) -Target $instance -ErrorRecord $_ + } + if ($result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = $null + Result = Select-DefaultView -InputObject $result -Property * + } + + } + } + else { + # if running WhatIf, then return the queries that would be run as an object, not just whatif output + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "WhatIf - Bypassed Execution" + Result = $null + } + } + + } + elseif ($scriptpart.DBSpecific -and !$instanceOnly) { + + foreach ($currentdb in $databases) { + if ($ExportQueries) { + $null = New-Item -Path $OutputPath -ItemType Directory -Force + $FileName = Remove-InvalidFileNameChars ('{0}-{1}-{2}.sql' -f $server.DomainInstanceName, $currentDb, $Scriptpart.QueryName) + $FullName = Join-Path $OutputPath $FileName + Write-Message -Level Verbose -Message "Creating file: $FullName" + $scriptPart.Text | out-file -FilePath $FullName -encoding UTF8 -force + continue + } + + + if ($PSCmdlet.ShouldProcess(('{0} ({1})' -f $instance, $currentDb), $scriptpart.QueryName)) { + + if (-not $EnableException) { + $Counter++ + Write-Progress -Id $ProgressId -ParentId 0 -Activity "Collecting diagnostic query data from $($currentDb) on $instance" -Status ('Processing {0} of {1}' -f $counter, $scriptcount) -CurrentOperation $scriptpart.QueryName -PercentComplete (($Counter / $scriptcount) * 100) + } + + Write-Message -Level Verbose -Message "Collecting diagnostic query data from $($currentDb) for $($scriptpart.QueryName) on $instance" + try { + $result = $server.Query($scriptpart.Text, $currentDb) + if (!$result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "Empty Result for this Query" + Result = $null + } + Write-Message -Level Verbose -Message ("Empty result for Query {0} - {1} - {2}" -f $scriptpart.QueryNr, $scriptpart.QueryName, $scriptpart.Description) -Target $scriptpart -ErrorRecord $_ + } + } + catch { + Write-Message -Level Verbose -Message ('Some error has occured on Server: {0} - Script: {1} - Database: {2}, result will not be saved' -f $instance, $scriptpart.QueryName, $currentDb) -Target $currentdb -ErrorRecord $_ + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $currentDb + Notes = $null + Result = Select-DefaultView -InputObject $result -Property * + } + } + else { + # if running WhatIf, then return the queries that would be run as an object, not just whatif output + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "WhatIf - Bypassed Execution" + Result = $null + } + } + } + } + } + } + } + end { + Write-Progress -Id $ProgressId -Activity 'Invoke-DbaDiagnosticQuery' -Completed + } +} +function Invoke-DbaLogShipping { + <# + .SYNOPSIS + Invoke-DbaLogShipping sets up log shipping for one or more databases + + .DESCRIPTION + Invoke-DbaLogShipping helps to easily set up log shipping for one or more databases. + + This function will make a lot of decisions for you assuming you want default values like a daily interval for the schedules with a 15 minute interval on the day. + There are some settings that cannot be made by the function and they need to be prepared before the function is executed. + + The following settings need to be made before log shipping can be initiated: + - Backup destination (the folder and the privileges) + - Copy destination (the folder and the privileges) + + * Privileges + Make sure your agent service on both the primary and the secondary instance is an Active Directory account. + Also have the credentials ready to set the folder permissions + + ** Network share + The backup destination needs to be shared and have the share privileges of FULL CONTROL to Everyone. + + ** NTFS permissions + The backup destination must have at least read/write permissions for the primary instance agent account. + The backup destination must have at least read permissions for the secondary instance agent account. + The copy destination must have at least read/write permission for the secondary instance agent acount. + + .PARAMETER SourceSqlInstance + Source SQL Server instance which contains the databases to be log shipped. + You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER DestinationSqlInstance + Destination SQL Server instance which contains the databases to be log shipped. + You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER SourceCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Database to set up log shipping for. + + .PARAMETER BackupNetworkPath + The backup unc path to place the backup files. This is the root directory. + A directory with the name of the database will be created in this path. + + .PARAMETER BackupLocalPath + If the backup path is locally for the source server you can also set this value. + + .PARAMETER BackupJob + Name of the backup that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the backup job name. + The default is "LSBackup_[databasename]" + + .PARAMETER BackupRetention + The backup retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER BackupSchedule + Name of the backup schedule created for the backup job. + The parameter works as a prefix where the name of the database will be added to the backup job schedule name. + Default is "LSBackupSchedule_[databasename]" + + .PARAMETER BackupScheduleDisabled + Parameter to set the backup schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER BackupScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER BackupScheduleFrequencyInterval + The number of type periods to occur between each execution of the backup job. + + .PARAMETER BackupScheduleFrequencySubdayType + Specifies the units for the sub-day FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER BackupScheduleFrequencySubdayInterval + The number of sub-day type periods to occur between each execution of the backup job. + + .PARAMETER BackupScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER BackupScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER BackupScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER BackupScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER BackupScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER BackupScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER BackupThreshold + Is the length of time, in minutes, after the last backup before a threshold alert error is raised. + The default is 60. + + .PARAMETER CompressBackup + Do the backups need to be compressed. By default the backups are not compressed. + + .PARAMETER CopyDestinationFolder + The path to copy the transaction log backup files to. This is the root directory. + A directory with the name of the database will be created in this path. + + .PARAMETER CopyJob + Name of the copy job that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the copy job name. + The default is "LSBackup_[databasename]" + + .PARAMETER CopyRetention + The copy retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER CopySchedule + Name of the backup schedule created for the copy job. + The parameter works as a prefix where the name of the database will be added to the copy job schedule name. + Default is "LSCopy_[DestinationServerName]_[DatabaseName]" + + .PARAMETER CopyScheduleDisabled + Parameter to set the copy schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER CopyScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER CopyScheduleFrequencyInterval + The number of type periods to occur between each execution of the copy job. + + .PARAMETER CopyScheduleFrequencySubdayType + Specifies the units for the subday FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER CopyScheduleFrequencySubdayInterval + The number of subday type periods to occur between each execution of the copy job. + + .PARAMETER CopyScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER CopyScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER CopyScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER CopyScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER CopyScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER CopyScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER DisconnectUsers + If this parameter is set in combinations of standby the users will be disconnected during restore. + + .PARAMETER FullBackupPath + Path to an existing full backup. Use this when an existing backup needs to used to initialize the database on the secondary instance. + + .PARAMETER GenerateFullBackup + If the database is not initialized on the secondary instance it can be done by creating a new full backup and + restore it for you. + + .PARAMETER HistoryRetention + Is the length of time in minutes in which the history is retained. + The default value is 14420 + + .PARAMETER NoRecovery + If this parameter is set the database will be in recovery mode. The database will not be readable. + This setting is default. + + .PARAMETER NoInitialization + If this parameter is set the secondary database will not be initialized. + The database needs to be on the secondary instance in recovery mode. + + .PARAMETER PrimaryMonitorServer + Is the name of the monitor server for the primary server. + The default is the name of the primary sql server. + + .PARAMETER PrimaryMonitorCredential + Allows you to login to enter a secure credential. Only needs to be used when the PrimaryMonitorServerSecurityMode is 0 or "sqlserver" + To use: $scred = Get-Credential, then pass $scred object to the -PrimaryMonitorCredential parameter. + + .PARAMETER PrimaryMonitorServerSecurityMode + The security mode used to connect to the monitor server for the primary server. Allowed values are 0, "sqlserver", 1, "windows" + The default is 1 or Windows. + + .PARAMETER PrimaryThresholdAlertEnabled + Enables the Threshold alert for the primary database + + .PARAMETER RestoreDataFolder + Folder to be used to restore the database data files. Only used when parameter GenerateFullBackup or UseExistingFullBackup are set. + If the parameter is not set the default data folder of the secondary instance will be used including the name of the database. + If the folder is set but doesn't exist the default data folder of the secondary instance will be used including the name of the database. + + .PARAMETER RestoreLogFolder + Folder to be used to restore the database log files. Only used when parameter GenerateFullBackup or UseExistingFullBackup are set. + If the parameter is not set the default transaction log folder of the secondary instance will be used. + If the folder is set but doesn't exist the default transaction log folder of the secondary instance will be used. + + .PARAMETER RestoreDelay + In case a delay needs to be set for the restore. + The default is 0. + + .PARAMETER RestoreAlertThreshold + The amount of minutes after which an alert will be raised is no restore has taken place. + The default is 45 minutes. + + .PARAMETER RestoreJob + Name of the restore job that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the restore job name. + The default is "LSRestore_[databasename]" + + .PARAMETER RestoreRetention + The backup retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER RestoreSchedule + Name of the backup schedule created for the restore job. + The parameter works as a prefix where the name of the database will be added to the restore job schedule name. + Default is "LSRestore_[DestinationServerName]_[DatabaseName]" + + .PARAMETER RestoreScheduleDisabled + Parameter to set the restore schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER RestoreScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER RestoreScheduleFrequencyInterval + The number of type periods to occur between each execution of the restore job. + + .PARAMETER RestoreScheduleFrequencySubdayType + Specifies the units for the subday FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER RestoreScheduleFrequencySubdayInterval + The number of subday type periods to occur between each execution of the restore job. + + .PARAMETER RestoreScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER RestoreScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER RestoreScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER RestoreScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER RestoreScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER RestoreScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER RestoreThreshold + The number of minutes allowed to elapse between restore operations before an alert is generated. + The default value = 0 + + .PARAMETER SecondaryDatabasePrefix + The secondary database can be renamed to include a prefix. + + .PARAMETER SecondaryDatabaseSuffix + The secondary database can be renamed to include a suffix. + + .PARAMETER SecondaryMonitorServer + Is the name of the monitor server for the secondary server. + The default is the name of the secondary sql server. + + .PARAMETER SecondaryMonitorCredential + Allows you to login to enter a secure credential. Only needs to be used when the SecondaryMonitorServerSecurityMode is 0 or "sqlserver" + To use: $scred = Get-Credential, then pass $scred object to the -SecondaryMonitorCredential parameter. + + .PARAMETER SecondaryMonitorServerSecurityMode + The security mode used to connect to the monitor server for the secondary server. Allowed values are 0, "sqlserver", 1, "windows" + The default is 1 or Windows. + + .PARAMETER SecondaryThresholdAlertEnabled + Enables the Threshold alert for the secondary database + + .PARAMETER Standby + If this parameter is set the database will be set to standby mode making the database readable. + If not set the database will be in recovery mode. + + .PARAMETER StandbyDirectory + Directory to place the standby file(s) in + + .PARAMETER UseExistingFullBackup + If the database is not initialized on the secondary instance it can be done by selecting an existing full backup + and restore it for you. + + .PARAMETER UseBackupFolder + This enables the user to specify a specific backup folder containing one or more backup files to initialize the database on the secondary instance. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + Use this switch to disable any kind of verbose messages + + .PARAMETER Force + The force parameter will ignore some errors in the parameters and assume defaults. + It will also remove the any present schedules with the same name for the specific job. + + .NOTES + Tags: LogShipping + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaLogShipping + + .EXAMPLE + $params = @{ + SourceSqlInstance = 'sql1' + DestinationSqlInstance = 'sql2' + Database = 'db1' + BackupNetworkPath= '\\sql1\logshipping' + BackupLocalPath= 'D:\Data\logshipping' + BackupScheduleFrequencyType = 'daily' + BackupScheduleFrequencyInterval = 1 + CompressBackup = $true + CopyScheduleFrequencyType = 'daily' + CopyScheduleFrequencyInterval = 1 + GenerateFullBackup = $true + RestoreScheduleFrequencyType = 'daily' + RestoreScheduleFrequencyInterval = 1 + SecondaryDatabaseSuffix = 'DR' + CopyDestinationFolder = '\\sql2\logshippingdest' + Force = $true + } + + Invoke-DbaLogShipping @params + + Sets up log shipping for database "db1" with the backup path to a network share allowing local backups. + It creates daily schedules for the backup, copy and restore job with all the defaults to be executed every 15 minutes daily. + The secondary database will be called "db1_LS". + + .EXAMPLE + $params = @{ + SourceSqlInstance = 'sql1' + DestinationSqlInstance = 'sql2' + Database = 'db1' + BackupNetworkPath= '\\sql1\logshipping' + GenerateFullBackup = $true + Force = $true + } + + Invoke-DbaLogShipping @params + + Sets up log shipping with all defaults except that a backup file is generated. + The script will show a message that the copy destination has not been supplied and asks if you want to use the default which would be the backup directory of the secondary server with the folder "logshipping" i.e. "D:\SQLBackup\Logshiping". + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("SourceServerInstance", "SourceSqlServerSqlServer", "Source")] + [object]$SourceSqlInstance, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("DestinationServerInstance", "DestinationSqlServer", "Destination")] + [object]$DestinationSqlInstance, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SourceSqlCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SourceCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $DestinationSqlCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $DestinationCredential, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Database, + + [parameter(Mandatory = $true)] + [string]$BackupNetworkPath, + + [parameter(Mandatory = $false)] + [string]$BackupLocalPath, + + [parameter(Mandatory = $false)] + [string]$BackupJob, + + [parameter(Mandatory = $false)] + [int]$BackupRetention, + + [parameter(Mandatory = $false)] + [string]$BackupSchedule, + + [parameter(Mandatory = $false)] + [switch]$BackupScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$BackupScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$BackupScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$BackupScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$BackupScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$BackupScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$BackupScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleEndTime, + + [parameter(Mandatory = $false)] + [int]$BackupThreshold, + + [parameter(Mandatory = $false)] + [switch]$CompressBackup, + + [parameter(Mandatory = $false)] + [string]$CopyDestinationFolder, + + [parameter(Mandatory = $false)] + [string]$CopyJob, + + [parameter(Mandatory = $false)] + [int]$CopyRetention, + + [parameter(Mandatory = $false)] + [string]$CopySchedule, + + [parameter(Mandatory = $false)] + [switch]$CopyScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$CopyScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$CopyScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$CopyScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$CopyScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$CopyScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$CopyScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleEndTime, + + [parameter(Mandatory = $false)] + [switch]$DisconnectUsers, + + [parameter(Mandatory = $false)] + [string]$FullBackupPath, + + [parameter(Mandatory = $false)] + [switch]$GenerateFullBackup, + + [parameter(Mandatory = $false)] + [int]$HistoryRetention, + + [parameter(Mandatory = $false)] + [switch]$NoRecovery, + + [parameter(Mandatory = $false)] + [switch]$NoInitialization, + + [Parameter(Mandatory = $false)] + [string]$PrimaryMonitorServer, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $PrimaryMonitorCredential, + + [Parameter(Mandatory = $false)] + [ValidateSet(0, "sqlserver", 1, "windows")] + [object]$PrimaryMonitorServerSecurityMode, + + [Parameter(Mandatory = $false)] + [switch]$PrimaryThresholdAlertEnabled, + + [parameter(Mandatory = $false)] + [string]$RestoreDataFolder, + + [parameter(Mandatory = $false)] + [string]$RestoreLogFolder, + + [parameter(Mandatory = $false)] + [int]$RestoreDelay, + + [parameter(Mandatory = $false)] + [int]$RestoreAlertThreshold, + + [parameter(Mandatory = $false)] + [string]$RestoreJob, + + [parameter(Mandatory = $false)] + [int]$RestoreRetention, + + [parameter(Mandatory = $false)] + [string]$RestoreSchedule, + + [parameter(Mandatory = $false)] + [switch]$RestoreScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$RestoreScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$RestoreScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$RestoreScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$RestoreScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$RestoreScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$RestoreScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleEndTime, + + [parameter(Mandatory = $false)] + [int]$RestoreThreshold, + + [parameter(Mandatory = $false)] + [string]$SecondaryDatabasePrefix, + + [parameter(Mandatory = $false)] + [string]$SecondaryDatabaseSuffix, + + [Parameter(Mandatory = $false)] + [string]$SecondaryMonitorServer, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SecondaryMonitorCredential, + + [Parameter(Mandatory = $false)] + [ValidateSet(0, "sqlserver", 1, "windows")] + [object]$SecondaryMonitorServerSecurityMode, + + [Parameter(Mandatory = $false)] + [switch]$SecondaryThresholdAlertEnabled, + + [parameter(Mandatory = $false)] + [switch]$Standby, + + [parameter(Mandatory = $false)] + [string]$StandbyDirectory, + + [parameter(Mandatory = $false)] + [switch]$UseExistingFullBackup, + + [parameter(Mandatory = $false)] + [string]$UseBackupFolder, + + [switch]$Force, + + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Write-Message -Message "Started log shipping for $SourceSqlInstance to $DestinationSqlInstance" -Level Output + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to source Sql Server $SourceSqlInstance.." -Level Output + try { + $SourceServer = Connect-SqlInstance -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $SourceSqlInstance" -ErrorRecord $_ -Target $SourceSqlInstance + return + } + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to destination Sql Server $DestinationSqlInstance.." -Level Output + try { + $DestinationServer = Connect-SqlInstance -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $DestinationSqlInstance" -ErrorRecord $_ -Target $DestinationSqlInstance + return + } + + # Check the instance if it is a named instance + $SourceServerName, $SourceInstanceName = $SourceSqlInstance.Split("\") + $DestinationServerName, $DestinationInstanceName = $DestinationSqlInstance.Split("\") + + if ($SourceInstanceName -eq $null) { + $SourceInstanceName = "MSSQLSERVER" + } + + if ($DestinationInstanceName -eq $null) { + $DestinationInstanceName = "MSSQLSERVER" + } + + $IsSourceLocal = $false + $IsDestinationLocal = $false + + # Check if it's local or remote + if ($SourceServerName -in ".", "localhost", $env:ServerName, "127.0.0.1") { + $IsSourceLocal = $true + } + if ($DestinationServerName -in ".", "localhost", $env:ServerName, "127.0.0.1") { + $IsDestinationLocal = $true + } + + # Set up regex strings for several checks + $RegexDate = '(?:`"/\\|?*]+)+$' + + # Check the instance names and the database settings + if (($SourceSqlInstance -eq $DestinationSqlInstance) -and (-not $SecondaryDatabasePrefix -or $SecondaryDatabaseSuffix)) { + Stop-Function -Message "The destination database is the same as the source`nPlease enter a prefix or suffix using -SecondaryDatabasePrefix or -SecondaryDatabaseSuffix." -Target $SourceSqlInstance + return + } + + # Check the connection timeout + if ($SourceServer.ConnectionContext.StatementTimeout -ne 0) { + $SourceServer.ConnectionContext.StatementTimeout = 0 + Write-Message -Message "Connection timeout of $SourceServer is set to 0" -Level Verbose + } + + if ($DestinationServer.ConnectionContext.StatementTimeout -ne 0) { + $DestinationServer.ConnectionContext.StatementTimeout = 0 + Write-Message -Message "Connection timeout of $DestinationServer is set to 0" -Level Verbose + } + + # Check the backup network path + Write-Message -Message "Testing backup network path $BackupNetworkPath" -Level Verbose + if ((Test-DbaSqlPath -Path $BackupNetworkPath -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + Stop-Function -Message "Backup network path $BackupNetworkPath is not valid or can't be reached." -Target $SourceSqlInstance + return + } + elseif ($BackupNetworkPath -notmatch $RegexUnc) { + Stop-Function -Message "Backup network path $BackupNetworkPath has to be in the form of \\server\share." -Target $SourceSqlInstance + return + } + + # Check the copy destination + if (-not $CopyDestinationFolder) { + # Make a default copy destination by retrieving the backup folder and adding a directory + $CopyDestinationFolder = "$($DestinationServer.Settings.BackupDirectory)\Logshipping" + + # Check to see if the path already exists + Write-Message -Message "Testing copy destination path $CopyDestinationFolder" -Level Verbose + if (Test-DbaSqlPath -Path $CopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) { + Write-Message -Message "Copy destination $CopyDestinationFolder already exists" -Level Verbose + } + else { + # Check if force is being used + if (-not $Force) { + # Set up the confirm part + $message = "The copy destination is missing. Do you want to use the default $($CopyDestinationFolder)?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Try to create the new directory + try { + # If the destination server is remote and the credential is set + if (-not $IsDestinationLocal -and $DestinationCredential) { + Invoke-Command2 -ComputerName $DestinationServerName -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + # If the server is local and the credential is set + elseif ($DestinationCredential) { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + # If the server is local and the credential is not set + else { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -Force:$Force -ItemType Directory | Out-Null + } + Write-Message -Message "Copy destination $CopyDestinationFolder created." -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong creating the copy destination folder $CopyDestinationFolder. `n$_" -Target $DestinationSqlInstance -ErrorRecord $_ + return + } + } + 1 { + Stop-Function -Message "Copy destination is a mandatory parameter. Please make sure the value is entered." -Target $DestinationSqlInstance + return + } + } # switch + } # if not force + else { + # Try to create the copy destination on the local server + try { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + Write-Message -Message "Copy destination $CopyDestinationFolder created." -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong creating the copy destination folder $CopyDestinationFolder. `n$_" -Target $DestinationSqlInstance -ErrorRecord $_ + return + } + } # else not force + } # if test path copy destination + } # if not copy destination + + Write-Message -Message "Testing copy destination path $CopyDestinationFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $CopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message "Copy destination folder $CopyDestinationFolder is not valid or can't be reached." -Target $DestinationSqlInstance + return + } + elseif ($CopyDestinationFolder.StartsWith("\\") -and $CopyDestinationFolder -notmatch $RegexUnc) { + Stop-Function -Message "Copy destination folder $CopyDestinationFolder has to be in the form of \\server\share." -Target $DestinationSqlInstance + return + } + + # Check the backup compression + if ($SourceServer.Version.Major -gt 9) { + if ($CompressBackup) { + Write-Message -Message "Setting backup compression to 1." -Level Verbose + [bool]$BackupCompression = 1 + } + else { + $backupServerSetting = (Get-DbaSpConfigure -SqlInstance $SourceSqlInstance -ConfigName DefaultBackupCompression).ConfiguredValue + Write-Message -Message "Setting backup compression to default server setting $backupServerSetting." -Level Verbose + [bool]$BackupCompression = $backupServerSetting + } + } + else { + Write-Message -Message "Source server $SourceServer does not support backup compression" -Level Verbose + } + + # Check the database parameter + if ($Database) { + foreach ($db in $Database) { + if ($db -notin $SourceServer.Databases.Name) { + Stop-Function -Message "Database $db cannot be found on instance $SourceSqlInstance" -Target $SourceSqlInstance + } + + $DatabaseCollection = $SourceServer.Databases | Where-Object { $_.Name -in $Database } + } + } + else { + Stop-Function -Message "Please supply a database to set up log shipping for" -Target $SourceSqlInstance -Continue + } + + # Set the database mode + if ($Standby) { + $DatabaseStatus = 1 + Write-Message -Message "Destination database status set to STANDBY" -Level Verbose + } + else { + $DatabaseStatus = 0 + Write-Message -Message "Destination database status set to NO RECOVERY" -Level Verbose + } + + # Setting defaults + if (-not $BackupRetention) { + $BackupRetention = 4320 + Write-Message -Message "Backup retention set to $BackupRetention" -Level Verbose + } + if (-not $BackupThreshold) { + $BackupThreshold = 60 + Write-Message -Message "Backup Threshold set to $BackupThreshold" -Level Verbose + } + if (-not $CopyRetention) { + $CopyRetention = 4320 + Write-Message -Message "Copy retention set to $CopyRetention" -Level Verbose + } + if (-not $HistoryRetention) { + $HistoryRetention = 14420 + Write-Message -Message "History retention set to $HistoryRetention" -Level Verbose + } + if (-not $RestoreAlertThreshold) { + $RestoreAlertThreshold = 45 + Write-Message -Message "Restore alert Threshold set to $RestoreAlertThreshold" -Level Verbose + } + if (-not $RestoreDelay) { + $RestoreDelay = 0 + Write-Message -Message "Restore delay set to $RestoreDelay" -Level Verbose + } + if (-not $RestoreRetention) { + $RestoreRetention = 4320 + Write-Message -Message "Restore retention set to $RestoreRetention" -Level Verbose + } + if (-not $RestoreThreshold) { + $RestoreThreshold = 0 + Write-Message -Message "Restore Threshold set to $RestoreThreshold" -Level Verbose + } + if (-not $PrimaryMonitorServerSecurityMode) { + $PrimaryMonitorServerSecurityMode = 1 + Write-Message -Message "Primary monitor server security mode set to $PrimaryMonitorServerSecurityMode" -Level Verbose + } + if (-not $SecondaryMonitorServerSecurityMode) { + $SecondaryMonitorServerSecurityMode = 1 + Write-Message -Message "Secondary monitor server security mode set to $SecondaryMonitorServerSecurityMode" -Level Verbose + } + if (-not $BackupScheduleFrequencyType) { + $BackupScheduleFrequencyType = "Daily" + Write-Message -Message "Backup frequency type set to $BackupScheduleFrequencyType" -Level Verbose + } + if (-not $BackupScheduleFrequencyInterval) { + $BackupScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Backup frequency interval set to $BackupScheduleFrequencyInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencySubdayType) { + $BackupScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Backup frequency subday type set to $BackupScheduleFrequencySubdayType" -Level Verbose + } + if (-not $BackupScheduleFrequencySubdayInterval) { + $BackupScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Backup frequency subday interval set to $BackupScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencyRelativeInterval) { + $BackupScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Backup frequency relative interval set to $BackupScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencyRecurrenceFactor) { + $BackupScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Backup frequency recurrence factor set to $BackupScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not $CopyScheduleFrequencyType) { + $CopyScheduleFrequencyType = "Daily" + Write-Message -Message "Copy frequency type set to $CopyScheduleFrequencyType" -Level Verbose + } + if (-not $CopyScheduleFrequencyInterval) { + $CopyScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Copy frequency interval set to $CopyScheduleFrequencyInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencySubdayType) { + $CopyScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Copy frequency subday type set to $CopyScheduleFrequencySubdayType" -Level Verbose + } + if (-not $CopyScheduleFrequencySubdayInterval) { + $CopyScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Copy frequency subday interval set to $CopyScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencyRelativeInterval) { + $CopyScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Copy frequency relative interval set to $CopyScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencyRecurrenceFactor) { + $CopyScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Copy frequency recurrence factor set to $CopyScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not $RestoreScheduleFrequencyType) { + $RestoreScheduleFrequencyType = "Daily" + Write-Message -Message "Restore frequency type set to $RestoreScheduleFrequencyType" -Level Verbose + } + if (-not $RestoreScheduleFrequencyInterval) { + $RestoreScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Restore frequency interval set to $RestoreScheduleFrequencyInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencySubdayType) { + $RestoreScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Restore frequency subday type set to $RestoreScheduleFrequencySubdayType" -Level Verbose + } + if (-not $RestoreScheduleFrequencySubdayInterval) { + $RestoreScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Restore frequency subday interval set to $RestoreScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencyRelativeInterval) { + $RestoreScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Restore frequency relative interval set to $RestoreScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencyRecurrenceFactor) { + $RestoreScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Restore frequency recurrence factor set to $RestoreScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not ($SecondaryDatabasePrefix -or $SecondaryDatabaseSuffix) -and ($SourceServer.Name -eq $DestinationServer.Name) -and ($SourceServer.InstanceName -eq $DestinationServer.InstanceName)) { + if ($Force) { + $SecondaryDatabaseSuffix = "_LS" + } + else { + Stop-Function -Message "Destination database is the same as source database.`nPlease check the secondary server, database prefix or suffix or use -Force to set the secondary databse using a suffix." -Target $SourceSqlInstance + return + } + } + + # Checking for contradicting variables + if ($NoInitialization -and ($GenerateFullBackup -or $UseExistingFullBackup)) { + Stop-Function -Message "Cannot use -NoInitialization with -GenerateFullBackup or -UseExistingFullBackup" -Target $DestinationSqlInstance + return + } + + if ($UseBackupFolder -and ($GenerateFullBackup -or $NoInitialization -or $UseExistingFullBackup)) { + Stop-Function -Message "Cannot use -UseBackupFolder with -GenerateFullBackup, -NoInitialization or -UseExistingFullBackup" -Target $DestinationSqlInstance + return + } + + # Check the subday interval + if (($BackupScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($BackupScheduleFrequencySubdayInterval -ge 1 -or $BackupScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Backup subday interval $BackupScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $SourceSqlInstance + return + } + elseif (($BackupScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($BackupScheduleFrequencySubdayInterval -ge 1 -and $BackupScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Backup Subday interval $BackupScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $SourceSqlInstance + return + } + + # Check the subday interval + if (($CopyScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($CopyScheduleFrequencySubdayInterval -ge 1 -or $CopyScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Copy subday interval $CopyScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $DestinationSqlInstance + return + } + elseif (($CopyScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($CopyScheduleFrequencySubdayInterval -ge 1 -and $CopyScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Copy subday interval $CopyScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours'" -Target $DestinationSqlInstance + return + } + + # Check the subday interval + if (($RestoreScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($RestoreScheduleFrequencySubdayInterval -ge 1 -or $RestoreScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Restore subday interval $RestoreScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $DestinationSqlInstance + return + } + elseif (($RestoreScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($RestoreScheduleFrequencySubdayInterval -ge 1 -and $RestoreScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Restore subday interval $RestoreScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $DestinationSqlInstance + return + } + + # Check the backup start date + if (-not $BackupScheduleStartDate) { + $BackupScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Backup start date set to $BackupScheduleStartDate" -Level Verbose + } + else { + if ($BackupScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Backup start date $BackupScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the back start time + if (-not $BackupScheduleStartTime) { + $BackupScheduleStartTime = '000000' + Write-Message -Message "Backup start time set to $BackupScheduleStartTime" -Level Verbose + } + elseif ($BackupScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Backup start time $BackupScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the back end time + if (-not $BackupScheduleEndTime) { + $BackupScheduleEndTime = '235959' + Write-Message -Message "Backup end time set to $BackupScheduleEndTime" -Level Verbose + } + elseif ($BackupScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Backup end time $BackupScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the backup end date + if (-not $BackupScheduleEndDate) { + $BackupScheduleEndDate = '99991231' + } + elseif ($BackupScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Backup end date $BackupScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the copy start date + if (-not $CopyScheduleStartDate) { + $CopyScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Copy start date set to $CopyScheduleStartDate" -Level Verbose + } + else { + if ($CopyScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Copy start date $CopyScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the copy end date + if (-not $CopyScheduleEndDate) { + $CopyScheduleEndDate = '99991231' + } + elseif ($CopyScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Copy end date $CopyScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the copy start time + if (-not $CopyScheduleStartTime) { + $CopyScheduleStartTime = '000000' + Write-Message -Message "Copy start time set to $CopyScheduleStartTime" -Level Verbose + } + elseif ($CopyScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Copy start time $CopyScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the copy end time + if (-not $CopyScheduleEndTime) { + $CopyScheduleEndTime = '235959' + Write-Message -Message "Copy end time set to $CopyScheduleEndTime" -Level Verbose + } + elseif ($CopyScheduleEndTime -notmatch $RegexTime) { + Stop-Function -Message "Copy end time $CopyScheduleEndTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the restore start date + if (-not $RestoreScheduleStartDate) { + $RestoreScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Restore start date set to $RestoreScheduleStartDate" -Level Verbose + } + else { + if ($RestoreScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Restore start date $RestoreScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the restore end date + if (-not $RestoreScheduleEndDate) { + $RestoreScheduleEndDate = '99991231' + } + elseif ($RestoreScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Restore end date $RestoreScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the restore start time + if (-not $RestoreScheduleStartTime) { + $RestoreScheduleStartTime = '000000' + Write-Message -Message "Restore start time set to $RestoreScheduleStartTime" -Level Verbose + } + elseif ($RestoreScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Restore start time $RestoreScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the restore end time + if (-not $RestoreScheduleEndTime) { + $RestoreScheduleEndTime = '235959' + Write-Message -Message "Restore end time set to $RestoreScheduleEndTime" -Level Verbose + } + elseif ($RestoreScheduleEndTime -notmatch $RegexTime) { + Stop-Function -Message "Restore end time $RestoreScheduleEndTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check if standby is being used + if ($Standby) { + + # Check the stand-by directory + if ($StandbyDirectory) { + # Check if the path is reachable for the destination server + if ((Test-DbaSqlPath -Path $StandbyDirectory -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message "The directory $StandbyDirectory cannot be reached by the destination instance. Please check the permission and credentials." -Target $DestinationSqlInstance + return + } + } + elseif (-not $StandbyDirectory -and $Force) { + $StandbyDirectory = $DestinationSqlInstance.BackupDirectory + Write-Message -Message "Stand-by directory was not set. Setting it to $StandbyDirectory" -Level Verbose + } + else { + Stop-Function -Message "Please set the parameter -StandbyDirectory when using -Standby" -Target $SourceSqlInstance + return + } + } + } # begin + + process { + + if (Test-FunctionInterrupt) { return } + + # Loop through each of the databases + foreach ($db in $DatabaseCollection) { + + # Check the status of the database + if ($db.RecoveryModel -ne 'Full') { + Stop-Function -Message "Database $db is not in FULL recovery mode" -Target $SourceSqlInstance -Continue + } + + # Set the intital destination database + $SecondaryDatabase = $db.Name + + # Set the database prefix + if ($SecondaryDatabasePrefix) { + $SecondaryDatabase = "$SecondaryDatabasePrefix$($db.Name)" + } + + # Set the database suffix + if ($SecondaryDatabaseSuffix) { + $SecondaryDatabase += $SecondaryDatabaseSuffix + } + + # Check is the database is already initialized an check if the database exists on the secondary instance + if ($NoInitialization -and ($DestinationServer.Databases.Name -notcontains $SecondaryDatabase)) { + Stop-Function -Message "Database $SecondaryDatabase needs to be initialized before log shipping setting can continue." -Target $SourceSqlInstance -Continue + } + + # Check the local backup path + if ($BackupLocalPath) { + if ($BackupLocalPath.EndsWith("\")) { + $DatabaseBackupLocalPath = "$BackupLocalPath$($db.Name)" + } + else { + $DatabaseBackupLocalPath = "$BackupLocalPath\$($db.Name)" + } + } + else { + $BackupLocalPath = $BackupNetworkPath + + if ($BackupLocalPath.EndsWith("\")) { + $DatabaseBackupLocalPath = "$BackupLocalPath$($db.Name)" + } + else { + $DatabaseBackupLocalPath = "$BackupLocalPath\$($db.Name)" + } + } + Write-Message -Message "Backup local path set to $DatabaseBackupLocalPath." -Level Verbose + + # Setting the backup network path for the database + if ($BackupNetworkPath.EndsWith("\")) { + $DatabaseBackupNetworkPath = "$BackupNetworkPath$($db.Name)" + } + else { + $DatabaseBackupNetworkPath = "$BackupNetworkPath\$($db.Name)" + } + Write-Message -Message "Backup network path set to $DatabaseBackupNetworkPath." -Level Verbose + + + # Checking if the database network path exists + Write-Message -Message "Testing database backup network path $DatabaseBackupNetworkPath" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseBackupNetworkPath -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + # To to create the backup directory for the database + try { + Write-Message -Message "Database backup network path $DatabaseBackupNetworkPath not found. Trying to create it.." -Level Verbose + + Invoke-Command2 -Credential $SourceCredential -ScriptBlock { + Write-Message -Message "Creating backup folder $DatabaseBackupNetworkPath" -Level Verbose + New-Item -Path $DatabaseBackupNetworkPath -ItemType Directory -Credential $SourceCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + + # Check if the backup job name is set + if ($BackupJob) { + $DatabaseBackupJob = "$BackupJob_$($db.Name)" + } + else { + $DatabaseBackupJob = "LSBackup_$($db.Name)" + } + Write-Message -Message "Backup job name set to $DatabaseBackupJob" -Level Verbose + + # Check if the backup job schedule name is set + if ($BackupSchedule) { + $DatabaseBackupSchedule = "$BackupSchedule_$($db.Name)" + } + else { + $DatabaseBackupSchedule = "LSBackupSchedule_$($db.Name)" + } + Write-Message -Message "Backup job schedule name set to $DatabaseBackupSchedule" -Level Verbose + + # Check if secondary database is present on secondary instance + if (-not $Force -and -not $NoInitialization -and ($DestinationServer.Databases[$SecondaryDatabase].Status -ne 'Restoring') -and ($DestinationServer.Databases.Name -contains $SecondaryDatabase)) { + Stop-Function -Message "Secondary database already exists on instance $DestinationSqlInstance." -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + + # Check if the secondary database needs tobe initialized + if (-not $NoInitialization) { + # Check if the secondary database exists on the secondary instance + if ($DestiationServer.Databases.Name -notcontains $SecondaryDatabase) { + # Check if force is being used and no option to generate the full backup is set + if ($Force -and -not ($GenerateFullBackup -or $UseExistingFullBackup)) { + # Set the option to generate a full backup + Write-Message -Message "Set option to initialize secondary database with full backup" -Level Verbose + $GenerateFullBackup = $true + } + elseif (-not $Force -and -not $GenerateFullBackup -and -not $UseExistingFullBackup -and -not $UseBackupFolder) { + # Set up the confirm part + $message = "The database $SecondaryDatabase does not exist on instance $DestinationSqlInstance. `nDo you want to initialize it by generating a full backup?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Set the option to generate a full backup + Write-Message -Message "Set option to initialize secondary database with full backup." -Level Verbose + $GenerateFullBackup = $true + } + 1 { + Stop-Function -Message "The database is not initialized on the secondary instance. `nPlease initialize the database on the secondary instance, use -GenerateFullbackup or use -Force." -Target $DestinationSqlInstance + return + } + } # switch + } + } + } + + + # Check the parameters for initialization of the secondary database + if (-not $NoInitialization -and ($GenerateFullBackup -or $UseExistingFullBackup -or $UseBackupFolder)) { + # Check if the restore data and log folder are set + if (-not $RestoreDataFolder -or -not $RestoreLogFolder) { + Write-Message -Message "Restore data folder or restore log folder are not set. Using server defaults" -Level Verbose + + # Get the default data folder + if (-not $RestoreDataFolder) { + $DatabaseRestoreDataFolder = $DestinationServer.DefaultFile + } + else { + # Set the restore data folder + if ($RestoreDataFolder.EndsWith("\")) { + $DatabaseRestoreDataFolder = "$RestoreDataFolder$($db.Name)" + } + else { + $DatabaseRestoreDataFolder = "$RestoreDataFolder\$($db.Name)" + } + } + + Write-Message -Message "Restore data folder set to $DatabaseRestoreDataFolder" -Level Verbose + + # Get the default log folder + if (-not $RestoreLogFolder) { + $DatabaseRestoreLogFolder = $DestinationServer.DefaultLog + } + + Write-Message -Message "Restore log folder set to $DatabaseRestoreLogFolder" -Level Verbose + + # Check if the restore data folder exists + Write-Message -Message "Testing database restore data path $DatabaseRestoreDataFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseRestoreDataFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating database restore data folder $DatabaseRestoreDataFolder on $DestinationServerName")) { + # Try creating the data folder + try { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating data folder $DatabaseRestoreDataFolder" -Level Verbose + New-Item -Path $DatabaseRestoreDataFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the restore data directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + } + + # Check if the restore log folder exists + Write-Message -Message "Testing database restore log path $DatabaseRestoreLogFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseRestoreLogFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating database restore log folder $DatabaseRestoreLogFolder on $DestinationServerName")) { + # Try creating the log folder + try { + Write-Message -Message "Restore log folder $DatabaseRestoreLogFolder not found. Trying to create it.." -Level Verbose + + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Restore log folder $DatabaseRestoreLogFolder not found. Trying to create it.." -Level Verbose + New-Item -Path $DatabaseRestoreLogFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the restore log directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + } + } + + # Chech if the full backup patk can be reached + if ($FullBackupPath) { + Write-Message -Message "Testing full backup path $FullBackupPath" -Level Verbose + if ((Test-DbaSqlPath -Path $FullBackupPath -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message ("The path to the full backup could not be reached. Check the path and/or the crdential") -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + } + elseif ($UseBackupFolder.Length -ge 1) { + Write-Message -Message "Testing backup folder $UseBackupFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $UseBackupFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message ("The path to the backup folder could not be reached. Check the path and/or the crdential") -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + + $BackupPath = $UseBackupFolder + } + elseif ($UseExistingFullBackup) { + Write-Message -Message "No path to the full backup is set. Trying to retrieve the last full backup for $db from $SourceSqlInstance" -Level Verbose + + # Get the last full backup + $LastBackup = Get-DbaBackupHistory -SqlServer $SourceSqlInstance -Databases $($db.Name) -LastFull -Credential $SourceSqlCredential + + # Check if there was a last backup + if ($LastBackup -ne $null) { + # Test the path to the backup + Write-Message -Message "Testing last backup path $(($LastBackup[-1]).Path[-1])" -Level Verbose + if ((Test-DbaSqlPath -Path ($LastBackup[-1]).Path[-1] -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + Stop-Function -Message "The full backup could not be found on $($LastBackup.Path). Check path and/or credentials" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + # Check if the source for the last full backup is remote and the backup is on a shared location + elseif (($LastBackup.Computername -ne $SourceServerName) -and (($LastBackup[-1]).Path[-1].StartsWith('\\') -eq $false)) { + Stop-Function -Message "The last full backup is not located on shared location. `n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + else { + #$FullBackupPath = $LastBackup.Path + $BackupPath = $LastBackup.Path + Write-Message -Message "Full backup found for $db. Path $BackupPath" -Level Verbose + } + } + else { + Write-Message -Message "No Full backup found for $db." -Level Output + } + } + } + + # Set the copy destination folder to include the database name + if ($CopyDestinationFolder.EndsWith("\")) { + $DatabaseCopyDestinationFolder = "$CopyDestinationFolder$($db.Name)" + } + else { + $DatabaseCopyDestinationFolder = "$CopyDestinationFolder\$($db.Name)" + } + Write-Message -Message "Copy destination folder set to $DatabaseCopyDestinationFolder." -Level Verbose + + # Check if the copy job name is set + if ($CopyJob) { + $DatabaseCopyJob = "$CopyJob_$SourceServerName_$($db.Name)" + } + else { + $DatabaseCopyJob = "LSCopy_$SourceServerName_$($db.Name)" + } + Write-Message -Message "Copy job name set to $DatabaseCopyJob" -Level Verbose + + # Check if the copy job schedule name is set + if ($CopySchedule) { + $DatabaseCopySchedule = "$CopySchedule_$($db.Name)" + } + else { + $DatabaseCopySchedule = "LSCopySchedule_$($db.Name)" + Write-Message -Message "Copy job schedule name set to $DatabaseCopySchedule" -Level Verbose + } + + # Check if the copy destination folder exists + Write-Message -Message "Testing database copy destination path $DatabaseCopyDestinationFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseCopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating copy destination folder on $DestinationServerName")) { + try { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Copy destination folder $DatabaseCopyDestinationFolder not found. Trying to create it.. ." -Level Verbose + New-Item -Path $DatabaseCopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the database copy destination folder. `n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationServerName -Continue + } + } + } + + # Check if the restore job name is set + if ($RestoreJob) { + $DatabaseRestoreJob = "$RestoreJob_$SourceServerName_$($db.Name)" + } + else { + $DatabaseRestoreJob = "LSRestore_$DestinationServerName_$($db.Name)" + } + Write-Message -Message "Restore job name set to $DatabaseRestoreJob" -Level Verbose + + # Check if the restore job schedule name is set + if ($RestoreSchedule) { + $DatabaseRestoreSchedule = "$RestoreSchedule_$($db.Name)" + } + else { + $DatabaseRestoreSchedule = "LSRestoreSchedule_$($db.Name)" + } + Write-Message -Message "Restore job schedule name set to $DatabaseRestoreSchedule" -Level Verbose + + # If the database needs to be backed up first + if ($GenerateFullBackup) { + if ($PSCmdlet.ShouldProcess($SourceSqlInstance, "Backing up database $db")) { + + Write-Message -Message "Generating full backup." -Level Output + Write-Message -Message "Backing up database $db to $DatabaseBackupNetworkPath" -Level Output + + try { + $Timestamp = Get-Date -format "yyyyMMddHHmmss" + + $LastBackup = Backup-DbaDatabase -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -BackupDirectory $DatabaseBackupNetworkPath ` + -BackupFileName "FullBackup_$($db.Name)_PreLogShipping_$Timestamp.bak" ` + -Databases $($db.Name) ` + -Type Full + + Write-Message -Message "Backup completed." -Level Output + + # Get the last full backup path + #$FullBackupPath = $LastBackup.BackupPath + $BackupPath = $LastBackup.BackupPath + + Write-Message -Message "Backup is located at $BackupPath" -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong generating the full backup" -ErrorRecord $_ -Target $DestinationServerName -Continue + } + } + } + + # Check of the MonitorServerSecurityMode value is of type string and set the integer value + if ($PrimaryMonitorServerSecurityMode -notin 0, 1) { + $PrimaryMonitorServerSecurityMode = switch ($PrimaryMonitorServerSecurityMode) { + "SQLSERVER" { 0 } "WINDOWS" { 1 } default { 1 } + } + } + + # Check the primary monitor server + if ($Force -and (-not$PrimaryMonitorServer -or [string]$PrimaryMonitorServer -eq '' -or $PrimaryMonitorServer -eq $null)) { + Write-Message -Message "Setting monitor server for primary server to $SourceSqlInstance." -Level Output + $PrimaryMonitorServer = $SourceSqlInstance + } + + # Check the PrimaryMonitorServerSecurityMode if it's SQL Server authentication + if ($PrimaryMonitorServerSecurityMode -eq 0) { + if ($PrimaryMonitorServerLogin) { + Stop-Function -Message "The PrimaryMonitorServerLogin cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + + if ($PrimaryMonitorServerPassword) { + Stop-Function -Message "The PrimaryMonitorServerPassword cannot be empty when using SQL Server authentication." -Target $ -Continue + } + } + + # Check of the SecondaryMonitorServerSecurityMode value is of type string and set the integer value + if ($SecondaryMonitorServerSecurityMode -notin 0, 1) { + $SecondaryMonitorServerSecurityMode = switch ($SecondaryMonitorServerSecurityMode) { + "SQLSERVER" { 0 } "WINDOWS" { 1 } default { 1 } + } + } + + # Check the secondary monitor server + if ($Force -and (-not $SecondaryMonitorServer -or [string]$SecondaryMonitorServer -eq '' -or $SecondaryMonitorServer -eq $null)) { + Write-Message -Message "Setting secondary monitor server for $DestinationSqlInstance to $SourceSqlInstance." -Level Verbose + $SecondaryMonitorServer = $SourceSqlInstance + } + + # Check the MonitorServerSecurityMode if it's SQL Server authentication + if ($SecondaryMonitorServerSecurityMode -eq 0) { + if ($SecondaryMonitorServerLogin) { + Stop-Function -Message "The SecondaryMonitorServerLogin cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + + if ($SecondaryMonitorServerPassword) { + Stop-Function -Message "The SecondaryMonitorServerPassword cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + } + + # Now that all the checks have been done we can start with the fun stuff ! + + # Restore the full backup + if ($PSCmdlet.ShouldProcess($DestinationSqlInstance, "Restoring database $db to $SecondaryDatabase on $DestinationSqlInstance")) { + if ($GenerateFullBackup -or $UseExistingFullBackup -or $UseBackupFolder) { + try { + Write-Message -Message "Start database restore" -Level Output + if ($NoRecovery -or (-not $Standby)) { + if ($Force) { + Restore-DbaDatabase -SqlServer $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -NoRecovery ` + -WithReplace | Out-Null + } + else { + Restore-DbaDatabase -SqlServer $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -NoRecovery | Out-Null + } + } + + # If the database needs to be in standby + if ($Standby) { + # Setup the path to the standby file + $StandbyDirectory = "$DatabaseCopyDestinationFolder" + + # Check if credentials need to be used + if ($DestinationSqlCredential) { + Restore-DbaDatabase -ServerInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -StandbyDirectory $StandbyDirectory + } + else { + Restore-DbaDatabase -ServerInstance $DestinationSqlInstance ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -StandbyDirectory $StandbyDirectory + } + } + } + catch { + Stop-Function -Message "Something went wrong restoring the secondary database" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + + Write-Message -Message "Restore completed." -Level Output + } + } + + #region Set up log shipping on the primary instance + # Set up log shipping on the primary instance + if ($PSCmdlet.ShouldProcess($SourceSqlInstance, "Configuring logshipping for primary database $db on $SourceSqlInstance")) { + try { + + Write-Message -Message "Configuring logshipping for primary database" -Level Output + + New-DbaLogShippingPrimaryDatabase -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -Database $($db.Name) ` + -BackupDirectory $DatabaseBackupLocalPath ` + -BackupJob $DatabaseBackupJob ` + -BackupRetention $BackupRetention ` + -BackupShare $DatabaseBackupNetworkPath ` + -BackupThreshold $BackupThreshold ` + -CompressBackup:$BackupCompression ` + -HistoryRetention $HistoryRetention ` + -MonitorServer $PrimaryMonitorServer ` + -MonitorServerSecurityMode $PrimaryMonitorServerSecurityMode ` + -MonitorCredential $PrimaryMonitorCredential ` + -ThresholdAlertEnabled:$PrimaryThresholdAlertEnabled ` + -Force:$Force + + # Check if the backup job needs to be enabled or disabled + if ($BackupScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Job $DatabaseBackupJob -Disabled + Write-Message -Message "Disabling backup job $DatabaseBackupJob" -Level Output + } + else { + Set-DbaAgentJob -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Job $DatabaseBackupJob -Enabled + Write-Message -Message "Enabling backup job $DatabaseBackupJob" -Level Output + } + + Write-Message -Message "Create backup job schedule $DatabaseBackupSchedule" -Level Output + + $BackupJobSchedule = New-DbaAgentSchedule -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -Job $DatabaseBackupJob ` + -Schedule $DatabaseBackupSchedule ` + -FrequencyType $BackupScheduleFrequencyType ` + -FrequencyInterval $BackupScheduleFrequencyInterval ` + -FrequencySubdayType $BackupScheduleFrequencySubdayType ` + -FrequencySubdayInterval $BackupScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $BackupScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $BackupScheduleFrequencyRecurrenceFactor ` + -StartDate $BackupScheduleStartDate ` + -EndDate $BackupScheduleEndDate ` + -StartTime $BackupScheduleStartTime ` + -EndTime $BackupScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Configuring logshipping from primary to secondary database." -Level Output + + New-DbaLogShippingPrimarySecondary -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -PrimaryDatabase $($db.Name) ` + -SecondaryDatabase $SecondaryDatabase ` + -SecondaryServer $DestinationSqlInstance ` + -SecondarySqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Something went wrong setting up log shipping for primary instance" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + #endregion Set up log shipping on the primary instance + + #region Set up log shipping on the secondary instance + # Set up log shipping on the secondary instance + if ($PSCmdlet.ShouldProcess($DestinationSqlInstance, "Configuring logshipping for secondary database $SecondaryDatabase on $DestinationSqlInstance")) { + try { + + Write-Message -Message "Configuring logshipping from secondary database $SecondaryDatabase to primary database $db." -Level Output + + New-DbaLogShippingSecondaryPrimary -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -BackupSourceDirectory $DatabaseBackupNetworkPath ` + -BackupDestinationDirectory $DatabaseCopyDestinationFolder ` + -CopyJob $DatabaseCopyJob ` + -FileRetentionPeriod $BackupRetention ` + -MonitorServer $SecondaryMonitorServer ` + -MonitorServerSecurityMode $SecondaryMonitorServerSecurityMode ` + -MonitorCredential $SecondaryMonitorCredential ` + -PrimaryServer $SourceSqlInstance ` + -PrimaryDatabase $($db.Name) ` + -RestoreJob $DatabaseRestoreJob ` + -Force:$Force + + Write-Message -Message "Create copy job schedule $DatabaseCopySchedule" -Level Output + + $CopyJobSchedule = New-DbaAgentSchedule -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Job $DatabaseCopyJob ` + -Schedule $DatabaseCopySchedule ` + -FrequencyType $CopyScheduleFrequencyType ` + -FrequencyInterval $CopyScheduleFrequencyInterval ` + -FrequencySubdayType $CopyScheduleFrequencySubdayType ` + -FrequencySubdayInterval $CopyScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $CopyScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $CopyScheduleFrequencyRecurrenceFactor ` + -StartDate $CopyScheduleStartDate ` + -EndDate $CopyScheduleEndDate ` + -StartTime $CopyScheduleStartTime ` + -EndTime $CopyScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Create restore job schedule $DatabaseRestoreSchedule" -Level Output + + $RestoreJobSchedule = New-DbaAgentSchedule -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Job $DatabaseRestoreJob ` + -Schedule $DatabaseRestoreSchedule ` + -FrequencyType $RestoreScheduleFrequencyType ` + -FrequencyInterval $RestoreScheduleFrequencyInterval ` + -FrequencySubdayType $RestoreScheduleFrequencySubdayType ` + -FrequencySubdayInterval $RestoreScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $RestoreScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $RestoreScheduleFrequencyRecurrenceFactor ` + -StartDate $RestoreScheduleStartDate ` + -EndDate $RestoreScheduleEndDate ` + -StartTime $RestoreScheduleStartTime ` + -EndTime $RestoreScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Configuring logshipping for secondary database." -Level Output + + New-DbaLogShippingSecondaryDatabase -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -SecondaryDatabase $SecondaryDatabase ` + -PrimaryServer $SourceSqlInstance ` + -PrimaryDatabase $($db.Name) ` + -RestoreDelay $RestoreDelay ` + -RestoreMode $DatabaseStatus ` + -DisconnectUsers:$DisconnectUsers ` + -RestoreThreshold $RestoreThreshold ` + -ThresholdAlertEnabled:$SecondaryThresholdAlertEnabled ` + -HistoryRetention $HistoryRetention + + # Check if the copy job needs to be enabled or disabled + if ($CopyScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseCopyJob -Disabled + } + else { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseCopyJob -Enabled + } + + # Check if the restore job needs to be enabled or disabled + if ($RestoreScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseRestoreJob -Disabled + } + else { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseRestoreJob -Enabled + } + + } + catch { + Stop-Function -Message "Something went wrong setting up log shipping for secondary instance.`n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + } + #endregion Set up log shipping on the secondary instance + + Write-Message -Message "Completed configuring log shipping for database $db" -Level Output + + } # for each database + } # end process + + end { + Write-Message -Message "Finished setting up log shipping." -Level Verbose + } +} +function Invoke-DbaLogShippingRecovery { + <# + .SYNOPSIS + Invoke-DbaLogShippingRecovery recovers log shipped databases to a normal state to act upon a migration or disaster. + + .DESCRIPTION + By default all the databases for a particular instance are recovered. + If the database is in the right state, either standby or recovering, the process will try to recover the database. + + At first the function will check if the backup source directory can still be reached. + If so it will look up the last transaction log backup for the database. If that backup file is not the last copied file the log shipping copy job will be started. + If the directory cannot be reached for the function will continue to the restoring process. + After the copy job check is performed the job is disabled to prevent the job to run. + + For the restore the log shipping status is checked in the msdb database. + If the last restored file is not the same as the last file name found, the log shipping restore job will be executed. + After the restore job check is performed the job is disabled to prevent the job to run + + The last part is to set the database online by restoring the databases with recovery + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to + + .PARAMETER Database + Database to perform the restore for. This value can also be piped enabling multiple databases to be recovered. + If this value is not supplied all databases will be recovered. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER NoRecovery + Allows you to choose to not restore the database to a functional state (Normal) in the final steps of the process. + By default the database is restored to a functional state (Normal). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + Use this parameter to force the function to continue and perform any adjusting actions to successfully execute + + .PARAMETER Delay + Set the delay in seconds to wait for the copy and/or restore jobs. + By default the delay is 5 seconds + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: LogShipping + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaLogShippingRecovery + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 + + Recovers all the databases on the instance that are enabled for log shipping + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -SqlCredential $cred -Verbose + + Recovers all the databases on the instance that are enabled for log shipping using a credential + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -database db_logship -Verbose + + Recovers the database "db_logship" to a normal status + + .EXAMPLE + db1, db2, db3, db4 | Invoke-DbaLogShippingRecovery -SqlServer server1 -Verbose + + Recovers the database db1, db2, db3, db4 to a normal status + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -WhatIf + + Shows what would happen if the command were executed. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [object]$SqlInstance, + [Parameter(ValueFromPipeline = $true)] + [object[]]$Database, + [PSCredential]$SqlCredential, + [switch]$NoRecovery, + [Alias('Silent')] + [switch]$EnableException, + [switch]$Force, + [int]$Delay = 5 + ) + + begin { + if (!$sqlinstance -and $database.Count -lt 1) { + # You can prolly do this with + Stop-Function -Message "You must pipe an SMO database object or specify SqlInstance" + return + } + + if ($sqlinstance) { + # Check the instance if it is a named instance + $servername, $instancename = $sqlinstance.Split("\") + + if ($null -eq $instancename) { + $instancename = "MSSQLSERVER" + } + + Write-Message -Message "Attempting to connect to Sql Server" -Level Output + try { + $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance + } + + if ($Force -and (!$database -or $database.Count -lt 1)) { + $database = $server.databases + } + elseif (-not $Force -and (!$database -or $database.Count -lt 1)) { + Stop-Function -Message "Please enter one or more databases to recover from log shipping" -Target $instance + } + else { + $databases = $server.databases | Where-Object Name -in $database + } + } + } + + process { + # Try to get the agent service details + try { + # Start the service + $agentservice = Get-DbaSqlService -ComputerName $servername | Where-Object {($_.ComputerName -eq $servername) -and ($_.DisplayName -eq "SQL Server Agent ($instancename)")} + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + + # Check if the service is running + if ($agentservice.State -ne 'Running') { + + if ($Force) { + try { + Start-DbaSqlService -ComputerName $servername -InstanceName $instancename -Type Agent -Credential $SqlCredential + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + } + # If the force switch and the silent switch are not set + elseif (!$Force -and !$EnableException) { + # Set up the parts for the user choice + $Title = "SQL Server Agent is not running" + $Info = "Do you want to start the SQL Server Agent service?" + + $Options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Start", "&Quit") + [int]$Defaultchoice = 0 + $choice = $host.UI.PromptForChoice($Title, $Info, $Options, $Defaultchoice) + + # Check the given option + if ($choice -eq 0) { + try { + # Start the service + Start-DbaSqlService -ComputerName $servername -InstanceName $instancename -Type Agent -Credential $SqlCredential + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + } + else { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + } + # If the force switch it not set and the silent switch is set + elseif (!$Force -and $EnableException) { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + # If nothing else matches and the agent service is not started + else { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + + } + + Write-Message -Message "Started Log Shipping Recovery" -Level Output + + # Loop through all the databases + foreach ($db in $databases) { + # Query for retrieving the log shipping information + $query = "SELECT lss.primary_server, lss.primary_database, lsd.secondary_database, lss.backup_source_directory, + lss.backup_destination_directory, lss.last_copied_file, lss.last_copied_date, + lsd.last_restored_file, sj1.name AS 'copyjob', sj2.name AS 'restorejob' + FROM msdb.dbo.log_shipping_secondary AS lss + INNER JOIN msdb.dbo.log_shipping_secondary_databases AS lsd ON lsd.secondary_id = lss.secondary_id + INNER JOIN msdb.dbo.sysjobs AS sj1 ON sj1.job_id = lss.copy_job_id + INNER JOIN msdb.dbo.sysjobs AS sj2 ON sj2.job_id = lss.restore_job_id + WHERE lsd.secondary_database = '$($db.Name)'" + + # Retrieve the log shipping information from the secondary instance + try { + Write-Message -Message "Retrieving log shipping information from the secondary instance" -Level Verbose + $logshipping_details = $server.Query($query) + } + catch { + Stop-Function -Message "Error retrieving the log shipping details: $($_.Exception.Message)" -ErrorRecord $_ -Target $sqlinstance + return + } + + # Check if there are any databases to recover + if ($null -eq $logshipping_details) { + Stop-Function -Message "The database $db is not configured as a secondary database for log shipping." -Continue + } + else { + # Loop through each of the log shipped databases + foreach ($ls in $logshipping_details) { + $secondarydb = $ls.secondary_database + + # Check if the database is in the right state + if ($server.Databases[$secondarydb].Status -notin ('Normal, Standby', 'Standby', 'Restoring')) { + Stop-Function -Message "The database $db doesn't have the right status to be recovered" -Continue + } + else { + Write-Message -Message "Started Recovery for $secondarydb" -Level Verbose + + # Get the last file from the backup source directory + <# !!!! set credentials !!! #> + $latestBackupSource = Get-ChildItem -Path $ls.backup_source_directory -filter ("*" + $ls.primary_database + "*") | Where-Object { ($_.Extension -eq '.trn') } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + # Get al the backup files from the destination directory + <# !!!! set credentials !!! #> + $latestBackupDest = Get-ChildItem -Path $ls.backup_destination_directory -filter ("*" + $ls.primary_database + "*") | Where-Object { ($_.Extension -eq '.trn') } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + # Check if source and destination directory are in sync + if ($latestBackupSource.Name -ne $latestBackupDest.Name) { + # Check if the backup source directory can be reached + if (Test-DbaSqlPath -SqlInstance $SqlInstance -Path $ls.backup_source_directory -SqlCredential $SqlCredential) { + + # Check if the latest file is also the latest copied file + if ($latestBackupSource.Name -ne ([string]$ls.last_copied_file).Split('\')[-1]) { + Write-Message -Message "Backup destination is not up-to-date" -Level Verbose + + # Start the job to get the latest files + if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting copy job $($ls.copyjob)"))) { + Write-Message -Message "Starting copy job $($ls.copyjob)" -Level Verbose + try { + $server.JobServer.Jobs[$ls.copyjob].Start() + } + catch { + Stop-Function -Message "Something went wrong starting the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + Write-Message -Message "Copying files to $($ls.backup_destination_directory)" -Level Verbose + + # Check if the file has been copied + $query = "SELECT last_copied_file FROM msdb.dbo.log_shipping_secondary WHERE primary_database = '$($ls.primary_database)' AND last_copied_file IS NOT NULL " + $latestcopy = $server.Query($query) + + Write-Message -Message "Waiting for the copy action to complete.." -Level Verbose + + while (($latestBackupSource.Name -ne ([string]$latestcopy.last_copied_file).Split('\')[-1])) { + # Sleep for while to let the files be copied + Start-Sleep -Seconds $Delay + + # Again get the latest file to check if the process can continue + $latestcopy = $server.Query($query) + } + + # Again get the latest file to check if the process can continue + $latestcopy = $server.Query($query) + + # Check the lat outcome of the job + if ($server.JobServer.Jobs[$ls.copyjob].LastRunOutcome -eq 'Failed') { + Stop-Function -Message "The copy job for database $db failed. Please check the error log." -Continue + } + + Write-Message -Message "Copying of backup files finished" -Level Verbose + } # if should process + } # if latest file name + } # if backup directory test + else { + Stop-Function -Message "Couldn't reach the backup source directory. Continuing..." -Continue + } + } # check latest backup file is already in directory + + + # Disable the log shipping copy job on the secondary instance + if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling copy job $($ls.copyjob)")) { + try { + Write-Message -Message "Disabling copy job $($ls.copyjob)" -Level Verbose + $server.JobServer.Jobs[$ls.copyjob].IsEnabled = $false + $server.JobServer.Jobs[$ls.copyjob].Alter() + } + catch { + Stop-Function -Message "Something went wrong disabling the copy job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + } + + # Check if the file has been copied + $query = "SELECT last_restored_file FROM msdb.dbo.log_shipping_secondary_databases WHERE secondary_database = '$secondarydb' AND last_restored_file IS NOT NULL" + $latestrestore = $server.Query($query) + + # Check if the last copied file is newer than the last restored file + if ((([string]$latestcopy.last_copied_file).Split('\')[-1] -ne ([string]$latestrestore.last_restored_file).Split('\')[-1]) -or ($null -eq ([string]$latestcopy.last_copied_file).Split('\')[-1])) { + Write-Message -Message "Restore is not up-to-date" -Level Verbose + + # Start the restore job + if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting restore job " + $ls.restorejob))) { + Write-Message -Message "Starting restore job $($ls.restorejob)" -Level Verbose + try { + $server.JobServer.Jobs[$ls.restorejob].Start() + } + catch { + Stop-Function -Message "Something went wrong starting the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + Write-Message -Message "Waiting for the restore action to complete.." -Level Verbose + + while ($latestBackupSource.Name -ne [string]($latestrestore.last_restored_file).Split('\')[-1]) { + # Sleep for while to let the files be copied + Start-Sleep -Seconds $Delay + + # Again get the latest file to check if the process can continue + $latestrestore = $server.Query($query) + } + + # Again get the latest file to check if the process can continue + $latestrestore = $server.Query($query) + + # Check the lat outcome of the job + if ($server.JobServer.Jobs[$ls.restorejob].LastRunOutcome -eq 'Failed') { + Stop-Function -Message "The restore job for database $db failed. Please check the error log." -Continue + } + } + } + + # Disable the log shipping restore job on the secondary instance + if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling restore job $($ls.restorejob)")) { + try { + Write-Message -Message ("Disabling restore job " + $ls.restorejob) -Level Verbose + $server.JobServer.Jobs[$ls.restorejob].IsEnabled = $false + $server.JobServer.Jobs[$ls.restorejob].Alter() + } + catch { + Stop-Function -Message "Something went wrong disabling the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + } + + # Check for the last time if everything is up-to-date + if ($latestBackupSource.Name -eq [string]($latestrestore.last_restored_file).Split('\')[-1]) { + # Check if the database needs to recovered to its normal state + if ($NoRecovery -eq $false) { + if ($PSCmdlet.ShouldProcess($secondarydb, "Restoring database with recovery")) { + Write-Message -Message "Restoring the database to it's normal state" -Level Verbose + $query = "RESTORE DATABASE [$secondarydb] WITH RECOVERY" + $server.Query($query) + } + } + else { + Write-Message -Message "Skipping restore with recovery" -Level Output + } + } + + Write-Message -Message ("Finished Recovery for $secondarydb") -Level Output + + # Reset the log ship details + $logshipping_details = $null + + } # database in restorable mode + } # foreach ls details + } # ls details are not null + } # foreach database + } # process +} +function Invoke-DbaPfRelog { + <# + .SYNOPSIS + Pipeline-compatible wrapper for the relog command which is available on modern Windows platforms. + + .DESCRIPTION + Pipeline-compatible wrapper for the relog command. Relog is useful for converting Windows Perfmon. + + Extracts performance counters from performance counter logs into other formats, + such as text-TSV (for tab-delimited text), text-CSV (for comma-delimited text), binary-BIN, or SQL. + + relog "C:\PerfLogs\Admin\System Correlation\WORKSTATIONX_20180112-000001\DataCollector01.blg" -o C:\temp\foo.csv -f tsv + + If you find any input hangs, please send us the output so we can accommodate for it then use -Raw for an immediate solution. + + .PARAMETER Path + Specifies the pathname of an existing performance counter log or performance counter path. You can specify multiple input files. + + .PARAMETER Destination + Specifies the pathname of the output file or SQL database where the counters will be written. Defaults to the same directory as the source. + + .PARAMETER Type + The output format. Defaults to tsv. Options include tsv, csv, bin, and sql. + + For a SQL database, the output file specifies the DSN!counter_log. You can specify the database location by using the ODBC manager to configure the DSN (Database System Name). + + For more information, read here: https://technet.microsoft.com/en-us/library/bb490958.aspx + + .PARAMETER Append + If this switch is enabled, output will be appended to the specified file instead of overwriting. This option does not apply to SQL format where the default is always to append. + + .PARAMETER AllowClobber + If this switch is enabled, the destination file will be overwritten if it exists. + + .PARAMETER PerformanceCounter + Specifies the performance counter path to log. + + .PARAMETER PerformanceCounterPath + Specifies the pathname of the text file that lists the performance counters to be included in a relog file. Use this option to list counter paths in an input file, one per line. Default setting is all counters in the original log file are relogged. + + .PARAMETER Interval + Specifies sample intervals in "n" records. Includes every nth data point in the relog file. Default is every data point. + + .PARAMETER BeginTime + This is is Get-Date object and we format it for you. + + .PARAMETER EndTime + Specifies end time for copying last record from the input file. This is is Get-Date object and we format it for you. + + .PARAMETER ConfigPath + Specifies the pathname of the settings file that contains command-line parameters. + + .PARAMETER Summary + If this switch is enabled, the performance counters and time ranges of log files specified in the input file will be displayed. + + .PARAMETER Multithread + If this switch is enabled, processing will be done in parallel. This may speed up large batches or large files. + + .PARAMETER AllTime + If this switch is enabled and a datacollector or datacollectorset is passed in via the pipeline, collects all logs, not just the latest. + + .PARAMETER Raw + If this switch is enabled, the results of the DOS command instead of Get-ChildItem will be displayed. This does not run in parallel. + + .PARAMETER InputObject + Accepts the output of Get-DbaPfDataCollector and Get-DbaPfDataCollectorSet as input via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter, Relog + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaPfRelog + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg + + Creates C:\temp\perfmon.tsv from C:\temp\perfmon.blg. + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg -Destination C:\temp\a\b\c + + Creates the temp, a, and b directories if needed, then generates c.tsv (tab separated) from C:\temp\perfmon.blg. + + Returns the newly created file as a file object. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2016 | Get-DbaPfDataCollector | Invoke-DbaPfRelog -Destination C:\temp\perf + + Creates C:\temp\perf if needed, then generates computername-datacollectorname.tsv (tab separated) from the latest logs of all data collector sets on sql2016. This destination format was chosen to avoid naming conflicts with piped input. + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg -Destination C:\temp\a\b\c -Raw + + Creates the temp, a, and b directories if needed, then generates c.tsv (tab separated) from C:\temp\perfmon.blg then outputs the raw results of the relog command. + + [Invoke-DbaPfRelog][21:21:35] relog "C:\temp\perfmon.blg" -f csv -o C:\temp\a\b\c + + Input + ---------------- + File(s): + C:\temp\perfmon.blg (Binary) + + Begin: 1/13/2018 5:13:23 + End: 1/13/2018 14:29:55 + Samples: 2227 + + 100.00% + + Output + ---------------- + File: C:\temp\a\b\c.csv + + Begin: 1/13/2018 5:13:23 + End: 1/13/2018 14:29:55 + Samples: 2227 + + The command completed successfully. + + .EXAMPLE + Invoke-DbaPfRelog -Path 'C:\temp\perflog with spaces.blg' -Destination C:\temp\a\b\c -Type csv -BeginTime ((Get-Date).AddDays(-30)) -EndTime ((Get-Date).AddDays(-1)) + + Creates the temp, a, and b directories if needed, then generates c.csv (comma separated) from C:\temp\perflog with spaces.blg', starts 30 days ago and ends one day ago. + + .EXAMPLE + $servers | Get-DbaPfDataCollectorSet | Get-DbaPfDataCollector | Invoke-DbaPfRelog -Multithread -AllowClobber + + Relogs latest data files from all collectors within the servers listed in $servers. + + .EXAMPLE + Get-DbaPfDataCollector -Collector DataCollector01 | Invoke-DbaPfRelog -AllowClobber -AllTime + + Relogs all the log files from the DataCollector01 on the local computer and allows overwrite. + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string]$Destination, + [ValidateSet("tsv", "csv", "bin", "sql")] + [string]$Type = "tsv", + [switch]$Append, + [switch]$AllowClobber, + [string[]]$PerformanceCounter, + [string]$PerformanceCounterPath, + [int]$Interval, + [datetime]$BeginTime, + [datetime]$EndTime, + [string]$ConfigPath, + [switch]$Summary, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$Multithread, + [switch]$AllTime, + [switch]$Raw, + [switch]$EnableException + ) + begin { + if (Test-Bound -ParameterName BeginTime) { + $script:beginstring = ($BeginTime -f 'M/d/yyyy hh:mm:ss' | Out-String).Trim() + } + if (Test-Bound -ParameterName EndTime) { + $script:endstring = ($EndTime -f 'M/d/yyyy hh:mm:ss' | Out-String).Trim() + } + + $allpaths = @() + $allpaths += $Path + + # to support multithreading + if (Test-Bound -ParameterName Destination) { + $script:destinationset = $true + $originaldestination = $Destination + } + else { + $script:destinationset = $false + } + } + process { + if ($Append -and $Type -ne "bin") { + Stop-Function -Message "Append can only be used with -Type bin." -Target $Path + return + } + + if ($InputObject) { + foreach ($object in $InputObject) { + # DataCollectorSet + if ($object.OutputLocation -and $object.RemoteOutputLocation) { + $instance = [dbainstance]$object.ComputerName + + if (-not $AllTime) { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.LatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteLatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + else { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.OutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + + + $script:perfmonobject = $true + } + # DataCollector + if ($object.LatestOutputLocation -and $object.RemoteLatestOutputLocation) { + $instance = [dbainstance]$object.ComputerName + + if (-not $AllTime) { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.LatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteLatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + else { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path (Split-Path $object.LatestOutputLocation) -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path (Split-Path $object.RemoteLatestOutputLocation) -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + $script:perfmonobject = $true + } + } + } + } + + # Gotta collect all the paths first then process them otherwise there may be duplicates + end { + $allpaths = $allpaths | Where-Object { $_ -match '.blg' } | Select-Object -Unique + + if (-not $allpaths) { + Stop-Function -Message "Could not find matching .blg files" -Target $file -Continue + return + } + + $scriptblock = { + if ($args) { + $file = $args + } + else { + $file = $psitem + } + $item = Get-ChildItem -Path $file -ErrorAction SilentlyContinue + + if ($null -eq $item) { + Stop-Function -Message "$file does not exist." -Target $file -Continue + return + } + + if (-not $script:destinationset -and $file -match "C\:\\.*Admin.*") { + $null = Test-ElevationRequirement -ComputerName $env:COMPUTERNAME -Continue + } + + if ($script:destinationset -eq $false -and -not $Append) { + $Destination = Join-Path (Split-Path $file) $item.BaseName + } + + if ($Destination -and $Destination -notmatch "\." -and -not $Append -and $script:perfmonobject) { + # if destination is set, then it needs a different name + if ($script:destinationset -eq $true) { + if ($file -match "\:") { + $computer = $env:COMPUTERNAME + } + else { + $computer = $file.Split("\")[2] + } + # Avoid naming conflicts + $timestamp = Get-Date -format yyyyMMddHHmmfff + $Destination = Join-Path $originaldestination "$computer - $($item.BaseName) - $timestamp" + } + } + + $params = @("`"$file`"") + + if ($Append) { + $params += "-a" + } + + if ($PerformanceCounter) { + $parsedcounters = $PerformanceCounter -join " " + $params += "-c `"$parsedcounters`"" + } + + if ($PerformanceCounterPath) { + $params += "-cf `"$PerformanceCounterPath`"" + } + + $params += "-f $Type" + + if ($Interval) { + $params += "-t $Interval" + } + + if ($Destination) { + $params += "-o `"$Destination`"" + } + + if ($script:beginstring) { + $params += "-b $script:beginstring" + } + + if ($script:endstring) { + $params += "-e $script:endstring" + } + + if ($ConfigPath) { + $params += "-config $ConfigPath" + } + + if ($Summary) { + $params += "-q" + } + + + if (-not ($Destination.StartsWith("DSN"))) { + $outputisfile = $true + } + else { + $outputisfile = $false + } + + if ($outputisfile) { + if ($Destination) { + $dir = Split-Path $Destination + if (-not (Test-Path -Path $dir)) { + try { + $null = New-Item -ItemType Directory -Path $dir -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $Destination -Continue + } + } + + if ((Test-Path $Destination) -and -not $Append -and ((Get-Item $Destination) -isnot [System.IO.DirectoryInfo])) { + if ($AllowClobber) { + try { + Remove-Item -Path "$Destination" -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Continue + } + } + else { + if ($Type -eq "bin") { + Stop-Function -Message "$Destination exists. Use -AllowClobber to overwrite or -Append to append." -Continue + } + else { + Stop-Function -Message "$Destination exists. Use -AllowClobber to overwrite." -Continue + } + } + } + + if ((Test-Path "$Destination.$type") -and -not $Append) { + if ($AllowClobber) { + try { + Remove-Item -Path "$Destination.$type" -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Continue + } + } + else { + if ($Type -eq "bin") { + Stop-Function -Message "$("$Destination.$type") exists. Use -AllowClobber to overwrite or -Append to append." -Continue + } + else { + Stop-Function -Message "$("$Destination.$type") exists. Use -AllowClobber to overwrite." -Continue + } + } + } + } + } + + $arguments = ($params -join " ") + + try { + if ($Raw) { + Write-Message -Level Output -Message "relog $arguments" + cmd /c "relog $arguments" + } + else { + Write-Message -Level Verbose -Message "relog $arguments" + $scriptblock = { + $output = (cmd /c "relog $arguments" | Out-String).Trim() + + if ($output -notmatch "Success") { + Stop-Function -Continue -Message $output.Trim("Input") + } + else { + Write-Message -Level Verbose -Message "$output" + $array = $output -Split [environment]::NewLine + $files = $array | Select-String "File:" + + foreach ($rawfile in $files) { + $rawfile = $rawfile.ToString().Replace("File:", "").Trim() + $gcierror = $null + Get-ChildItem $rawfile -ErrorAction SilentlyContinue -ErrorVariable gcierror | Add-Member -MemberType NoteProperty -Name RelogFile -Value $true -PassThru -ErrorAction Ignore + if ($gcierror) { + Write-Message -Level Verbose -Message "$gcierror" + } + } + } + } + Invoke-Command -ScriptBlock $scriptblock + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $path + } + } + + if ($Multithread) { + $allpaths | Invoke-Parallel -ImportVariables -ImportModules -ScriptBlock $scriptblock -ErrorAction SilentlyContinue -ErrorVariable parallelerror + if ($parallelerror) { + Write-Message -Level Verbose -Message "$parallelerror" + } + } + else { + foreach ($file in $allpaths) { Invoke-Command -ScriptBlock $scriptblock -ArgumentList $file } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Invoke-DbaSqlQuery { + <# + .SYNOPSIS + A command to run explicit T-SQL commands or files. + + .DESCRIPTION + This function is a wrapper command around Invoke-DbaSqlAsync, which in turn is based on Invoke-SqlCmd2. + It was designed to be more convenient to use in a pipeline and to behave in a way consistent with the rest of our functions. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server Instance as a different user. This can be a Windows or SQL Server account. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Database + The database to select before running the query. This list is auto-populated from the server. + + .PARAMETER Query + Specifies one or more queries to be run. The queries can be Transact-SQL, XQuery statements, or sqlcmd commands. Multiple queries in a single batch may be separated by a semicolon or a GO + + Escape any double quotation marks included in the string. + + Consider using bracketed identifiers such as [MyTable] instead of quoted identifiers such as "MyTable". + + .PARAMETER QueryTimeout + Specifies the number of seconds before the queries time out. + + .PARAMETER File + Specifies the path to one or several files to be used as the query input. + + .PARAMETER SqlObject + Specify on or multiple SQL objects. Those will be converted to script and their scripts run on the target system(s). + + .PARAMETER As + Specifies output type. Valid options for this parameter are 'DataSet', 'DataTable', 'DataRow', 'PSObject', and 'SingleValue' + + PSObject output introduces overhead but adds flexibility for working with results: http://powershell.org/wp/forums/topic/dealing-with-dbnull/ + + .PARAMETER SqlParameters + Specifies a hashtable of parameters for parameterized SQL queries. http://blog.codinghorror.com/give-me-parameterized-sql-or-give-me-death/ + + .PARAMETER AppendServerInstance + If this switch is enabled, the SQL Server instance will be appended to PSObject and DataRow output. + + .PARAMETER MessagesToOutput + Use this switch to have on the output stream messages too (e.g. PRINT statements). Output will hold the resultset too. See examples for detail + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Query + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaSqlQuery + + .EXAMPLE + Invoke-DbaSqlQuery -SqlInstance server\instance -Query 'SELECT foo FROM bar' + + Runs the sql query 'SELECT foo FROM bar' against the instance 'server\instance' + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance [SERVERNAME] -Group [GROUPNAME] | Invoke-DbaSqlQuery -Query 'SELECT foo FROM bar' + + Runs the sql query 'SELECT foo FROM bar' against all instances in the group [GROUPNAME] on the CMS [SERVERNAME] + + .EXAMPLE + "server1", "server1\nordwind", "server2" | Invoke-DbaSqlQuery -File "C:\scripts\sql\rebuild.sql" + + Runs the sql commands stored in rebuild.sql against the instances "server1", "server1\nordwind" and "server2" + + .EXAMPLE + Get-DbaDatabase -SqlInstance "server1", "server1\nordwind", "server2" | Invoke-DbaSqlQuery -File "C:\scripts\sql\rebuild.sql" + + Runs the sql commands stored in rebuild.sql against all accessible databases of the instances "server1", "server1\nordwind" and "server2" + #> + [CmdletBinding(DefaultParameterSetName = "Query")] + Param ( + [parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]] + $SqlInstance, + + [Alias("Credential")] + [PsCredential] + $SqlCredential, + + [object]$Database, + + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Query")] + [string] + $Query, + + [Int32] + $QueryTimeout = 600, + + [Parameter(Mandatory = $true, ParameterSetName = "File")] + [object[]] + $File, + + [Parameter(Mandatory = $true, ParameterSetName = "SMO")] + [Microsoft.SqlServer.Management.Smo.SqlSmoObject[]] + $SqlObject, + + [ValidateSet("DataSet", "DataTable", "DataRow", "PSObject", "SingleValue")] + [string] + $As = "DataRow", + + [System.Collections.IDictionary] + $SqlParameters, + + [switch] + $AppendServerInstance, + + [switch] + $MessagesToOutput, + + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + + [Alias('Silent')] + [switch] + $EnableException + + ) + + begin { + Write-Message -Level Debug -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $splatInvokeDbaSqlAsync = @{ + As = $As + } + + if (Test-Bound -ParameterName "SqlParameters") { + $splatInvokeDbaSqlAsync["SqlParameters"] = $SqlParameters + } + if (Test-Bound -ParameterName "AppendServerInstance") { + $splatInvokeDbaSqlAsync["AppendServerInstance"] = $AppendServerInstance + } + if (Test-Bound -ParameterName "Query") { + $splatInvokeDbaSqlAsync["Query"] = $Query + } + if (Test-Bound -ParameterName "QueryTimeout") { + $splatInvokeDbaSqlAsync["QueryTimeout"] = $QueryTimeout + } + if (Test-Bound -ParameterName "MessagesToOutput") { + $splatInvokeDbaSqlAsync["MessagesToOutput"] = $MessagesToOutput + } + if (Test-Bound -ParameterName "Verbose") { + $splatInvokeDbaSqlAsync["Verbose"] = $Verbose + } + + + if (Test-Bound -ParameterName "File") { + $files = @() + $temporaryFiles = @() + $temporaryFilesCount = 0 + $temporaryFilesPrefix = (97 .. 122 | Get-Random -Count 10 | ForEach-Object { [char]$_ }) -join '' + + foreach ($item in $File) { + if ($null -eq $item) { continue } + + $type = $item.GetType().FullName + + switch ($type) { + "System.IO.DirectoryInfo" { + if (-not $item.Exists) { + Stop-Function -Message "Directory not found!" -Category ObjectNotFound + return + } + $files += ($item.GetFiles() | Where-Object Extension -EQ ".sql").FullName + + } + "System.IO.FileInfo" { + if (-not $item.Exists) { + Stop-Function -Message "Directory not found!" -Category ObjectNotFound + return + } + + $files += $item.FullName + } + "System.String" { + $uri = [uri]$item + + switch -regex ($uri.Scheme) { + "http" { + $tempfile = "$env:TEMP\$temporaryFilesPrefix-$temporaryFilesCount.sql" + try { + Invoke-WebRequest -Uri $item -OutFile $tempfile -ErrorAction Stop + $files += $tempfile + $temporaryFilesCount++ + $temporaryFiles += $tempfile + } + catch { + Stop-Function -Message "Failed to download file $item" -ErrorRecord $_ + return + } + } + default { + try { + $paths = Resolve-Path $item | Select-Object -ExpandProperty Path | Get-Item -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to resolve path: $item" -ErrorRecord $_ + return + } + + foreach ($path in $paths) { + if (-not $path.PSIsContainer) { + if (([uri]$path.FullName).Scheme -ne 'file') { + Stop-Function -Message "Could not resolve path $path as filesystem object" + return + } + $files += $path.FullName + } + } + } + } + } + default { + Stop-Function -Message "Unkown input type: $type" -Category InvalidArgument + return + } + } + } + } + + if (Test-Bound -ParameterName "SqlObject") { + $files = @() + $temporaryFiles = @() + $temporaryFilesCount = 0 + $temporaryFilesPrefix = (97 .. 122 | Get-Random -Count 10 | ForEach-Object { [char]$_ }) -join '' + + foreach ($object in $SqlObject) { + try { $code = Export-DbaScript -InputObject $object -Passthru -EnableException } + catch { + Stop-Function -Message "Failed to generate script for object $object" -ErrorRecord $_ + return + } + + try { + $newfile = "$env:TEMP\$temporaryFilesPrefix-$temporaryFilesCount.sql" + Set-Content -Value $code -Path $newfile -Force -ErrorAction Stop -Encoding UTF8 + $files += $newfile + $temporaryFilesCount++ + $temporaryFiles += $newfile + } + catch { + Stop-Function -Message "Failed to write sql script to temp" -ErrorRecord $_ + return + } + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + if (Test-Bound -ParameterName "Database", "InputObject" -And) { + Stop-Function -Category InvalidArgument -Message "You can't use -Database with piped databases" + return + } + if (Test-Bound -ParameterName "SqlInstance", "InputObject" -And) { + Stop-Function -Category InvalidArgument -Message "You can't use -SqlInstance with piped databases" + return + } + + foreach ($db in $InputObject) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + $server = $db.Parent + $conncontext = $server.ConnectionContext + if ($conncontext.DatabaseName -ne $db.Name) { + $conncontext = $server.ConnectionContext.Copy() + $conncontext.DatabaseName = $db.Name + } + try { + if ($File -or $SqlObject) { + foreach ($item in $files) { + if ($null -eq $item) {continue} + $filePath = $(Resolve-Path -LiteralPath $item).ProviderPath + $QueryfromFile = [System.IO.File]::ReadAllText("$filePath") + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync -Query $QueryfromFile + } + } + else { Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync } + } + catch { + Stop-Function -Message "[$db] Failed during execution" -ErrorRecord $_ -Target $server -Continue + } + } + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $instance -Continue + } + $conncontext = $server.ConnectionContext + try { + if ($Database -and $conncontext.DatabaseName -ne $Database) { + $conncontext = $server.ConnectionContext.Copy() + $conncontext.DatabaseName = $Database + } + if ($File -or $SqlObject) { + foreach ($item in $files) { + if ($null -eq $item) {continue} + $filePath = $(Resolve-Path -LiteralPath $item).ProviderPath + $QueryfromFile = [System.IO.File]::ReadAllText("$filePath") + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync -Query $QueryfromFile + } + } + else { + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync + } + } + catch { + Stop-Function -Message "[$instance] Failed during execution" -ErrorRecord $_ -Target $instance -Continue + } + } + } + + end { + # Execute end even when interrupting, as only used for cleanup + + if ($temporaryFiles) { + # Clean up temporary files that were downloaded + foreach ($item in $temporaryFiles) { + Remove-Item -Path $item -ErrorAction Ignore + } + } + Test-DbaDeprecation -DeprecatedOn '1.0.0' -Alias Invoke-DbaSqlCmd + } +} +function Invoke-DbaWhoIsActive { + <# + .SYNOPSIS + Outputs results of Adam Machanic's sp_WhoIsActive DataTable + + .DESCRIPTION + Output results of Adam Machanic's sp_WhoIsActive + + This command was built with Adam's permission. To read more about sp_WhoIsActive, please visit: + + Updates: http://sqlblog.com/blogs/adam_machanic/archive/tags/who+is+active/default.aspx + + Also, consider donating to Adam if you find this stored procedure helpful: http://tinyurl.com/WhoIsActiveDonate + + .PARAMETER SqlInstance + The SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER Database + The database where sp_WhoIsActive is installed. Defaults to master. If the sp_WhoIsActive is not installed, the command will warn and exit. + + .PARAMETER Filter + FiltersBoth inclusive and exclusive + Set either filter to '' to disable + Session is a session ID, and either 0 or '' can be used to indicate "all" sessions + All other filter types support % or _ as wildcards + + .PARAMETER FilterType + Valid filter types are: session, program, database, login, and host + + .PARAMETER NotFilter + FiltersBoth inclusive and exclusive + Set either filter to '' to disable + Session is a session ID, and either 0 or '' can be used to indicate "all" sessions + All other filter types support % or _ as wildcards + + .PARAMETER NotFilterType + Valid filter types are: session, program, database, login, and host + + .PARAMETER ShowOwnSpid + Retrieve data about the calling session? + + .PARAMETER ShowSystemSpids + Retrieve data about system sessions? + + .PARAMETER ShowSleepingSpids + Controls how sleeping SPIDs are handled, based on the idea of levels of interest + 0 does not pull any sleeping SPIDs + 1 pulls only those sleeping SPIDs that also have an open transaction + 2 pulls all sleeping SPIDs + + .PARAMETER GetFullInnerText + If 1, gets the full stored procedure or running batch, when available + If 0, gets only the actual statement that is currently running in the batch or procedure + + .PARAMETER GetPlans + Get associated query plans for running tasks, if available + If 1, gets the plan based on the request's statement offset + If 2, gets the entire plan based on the request's plan_handle + + .PARAMETER GetOuterCommand + Get the associated outer ad hoc query or stored procedure call, if available + + .PARAMETER GetTransactionInfo + Enables pulling transaction log write info and transaction duration + + .PARAMETER GetTaskInfo + Get information on active tasks, based on three interest levels + Level 0 does not pull any task-related information + Level 1 is a lightweight mode that pulls the top non-CXPACKET wait, giving preference to blockers + Level 2 pulls all available task-based metrics, including: + number of active tasks, current wait stats, physical I/O, context switches, and blocker information + + .PARAMETER GetLocks + Gets associated locks for each request, aggregated in an XML format + + .PARAMETER GetAverageTime + Get average time for past runs of an active query + (based on the combination of plan handle, sql handle, and offset) + + .PARAMETER GetAdditonalInfo + Get additional non-performance-related information about the session or request + text_size, language, date_format, date_first, quoted_identifier, arithabort, ansi_null_dflt_on, + ansi_defaults, ansi_warnings, ansi_padding, ansi_nulls, concat_null_yields_null, + transaction_isolation_level, lock_timeout, deadlock_priority, row_count, command_type + + If a SQL Agent job is running, an subnode called agent_info will be populated with some or all of + the following: job_id, job_name, step_id, step_name, msdb_query_error (in the event of an error) + + If @get_task_info is set to 2 and a lock wait is detected, a subnode called block_info will be + populated with some or all of the following: lock_type, database_name, object_id, file_id, hobt_id, + applock_hash, metadata_resource, metadata_class_id, object_name, schema_name + + .PARAMETER FindBlockLeaders + Walk the blocking chain and count the number of + total SPIDs blocked all the way down by a given session + Also enables task_info Level 1, if @get_task_info is set to 0 + + .PARAMETER DeltaInterval + Pull deltas on various metrics + Interval in seconds to wait before doing the second data pull + + .PARAMETER OutputColumnList + List of desired output columns, in desired order + Note that the final output will be the intersection of all enabled features and all + columns in the list. Therefore, only columns associated with enabled features will + actually appear in the output. Likewise, removing columns from this list may effectively + disable features, even if they are turned on + + Each element in this list must be one of the valid output column names. Names must be + delimited by square brackets. White space, formatting, and additional characters are + allowed, as long as the list contains exact matches of delimited valid column names. + + .PARAMETER SortOrder + Column(s) by which to sort output, optionally with sort directions. + Valid column choices: + session_id, physical_io, reads, physical_reads, writes, tempdb_allocations, + tempdb_current, CPU, context_switches, used_memory, physical_io_delta, + reads_delta, physical_reads_delta, writes_delta, tempdb_allocations_delta, + tempdb_current_delta, CPU_delta, context_switches_delta, used_memory_delta, + tasks, tran_start_time, open_tran_count, blocking_session_id, blocked_session_count, + percent_complete, host_name, login_name, database_name, start_time, login_time + + Note that column names in the list must be bracket-delimited. Commas and/or white + space are not required. + + .PARAMETER FormatOutput + Formats some of the output columns in a more "human readable" form + 0 disables output format + 1 formats the output for variable-width fonts + 2 formats the output for fixed-width fonts + + .PARAMETER DestinationTable + If set to a non-blank value, the script will attempt to insert into the specified destination table. Please note that the script will not verify that the table exists, or that it has the correct schema, before doing the insert. Table can be specified in one, two, or three-part format + + .PARAMETER ReturnSchema + If set to 1, no data collection will happen and no result set will be returned; instead, + a CREATE TABLE statement will be returned via the @schema parameter, which will match + the schema of the result set that would be returned by using the same collection of the + rest of the parameters. The CREATE TABLE statement will have a placeholder token of + in place of an actual table name. + + .PARAMETER Schema + If set to 1, no data collection will happen and no result set will be returned; instead, + a CREATE TABLE statement will be returned via the @schema parameter, which will match + the schema of the result set that would be returned by using the same collection of the + rest of the parameters. The CREATE TABLE statement will have a placeholder token of + in place of an actual table name. + + .PARAMETER Help + Help! What do I do? + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: AdamMechanic, WhoIsActive, SpWhoIsActive + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaWhoIsActive + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a + + Execute sp_whoisactive on sqlserver2014a. This command expects sp_WhoIsActive to be in the master database. Logs into the SQL Server with Windows credentials. + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -SqlCredential $credential -Database dbatools + + Execute sp_whoisactive on sqlserver2014a. This command expects sp_WhoIsActive to be in the dbatools database. Logs into the SQL Server with SQL Authentication. + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -GetAverageTime + + Similar to running sp_WhoIsActive @get_avg_time + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -GetOuterCommand -FindBlockLeaders + + Similar to running sp_WhoIsActive @get_outer_command = 1, @find_block_leaders = 1 + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlServer')] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [object]$Database, + [Alias('As')] + [ValidateLength(0, 128)] + [string]$Filter, + [ValidateSet('Session', 'Program', 'Database', 'Login', 'Host')] + [string]$FilterType = 'Session', + [ValidateLength(0, 128)] + [string]$NotFilter, + [ValidateSet('Session', 'Program', 'Database', 'Login', 'Host')] + [string]$NotFilterType = 'Session', + [switch]$ShowOwnSpid, + [switch]$ShowSystemSpids, + [ValidateRange(0, 255)] + [int]$ShowSleepingSpids, + [switch]$GetFullInnerText, + [ValidateRange(0, 255)] + [int]$GetPlans, + [switch]$GetOuterCommand, + [switch]$GetTransactionInfo, + [ValidateRange(0, 2)] + [int]$GetTaskInfo, + [switch]$GetLocks, + [switch]$GetAverageTime, + [switch]$GetAdditonalInfo, + [switch]$FindBlockLeaders, + [ValidateRange(0, 255)] + [int]$DeltaInterval, + [ValidateLength(0, 8000)] + [string]$OutputColumnList = '[dd%][session_id][sql_text][sql_command][login_name][wait_info][tasks][tran_log%][cpu%][temp%][block%][reads%][writes%][context%][physical%][query_plan][locks][%]', + [ValidateLength(0, 500)] + [string]$SortOrder = '[start_time] ASC', + [ValidateRange(0, 255)] + [int]$FormatOutput = 1, + [ValidateLength(0, 4000)] + [string]$DestinationTable = '', + [switch]$ReturnSchema, + [string]$Schema, + [switch]$Help, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $passedparams = $psboundparameters.Keys | Where-Object { 'Silent', 'SqlServer', 'SqlCredential', 'OutputAs', 'ServerInstance', 'SqlInstance', 'Database' -notcontains $_ } + $localparams = $psboundparameters + } + + process { + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -lt 9) { + throw "sp_WhoIsActive is only supported in SQL Server 2005 and above" + } + + $paramdictionary = @{ + Filter = '@filter' + FilterType = '@filter_type' + NotFilter = 'not_filter' + NotFilterType = '@not_filter_type' + ShowOwnSpid = '@show_own_spid' + ShowSystemSpids = '@show_system_spids' + ShowSleepingSpids = '@show_sleeping_spids' + GetFullInnerText = '@get_full_inner_text' + GetPlans = '@get_plans' + GetOuterCommand = '@get_outer_command' + GetTransactionInfo = '@get_transaction_info' + GetTaskInfo = '@get_task_info' + GetLocks = '@get_locks ' + GetAverageTime = '@get_avg_time' + GetAdditonalInfo = '@get_additional_info' + FindBlockLeaders = '@find_block_leaders' + DeltaInterval = '@delta_interval' + OutputColumnList = '@output_column_list' + SortOrder = '@sort_order' + FormatOutput = '@format_output ' + DestinationTable = '@destination_table ' + ReturnSchema = '@return_schema' + Schema = '@schema' + Help = '@help' + } + + Write-Message -Level Verbose -Message "Collecting sp_whoisactive data from server: $instance" + + try { + $sqlconnection = New-Object System.Data.SqlClient.SqlConnection + $sqlconnection.ConnectionString = $server.ConnectionContext.ConnectionString + $sqlconnection.Open() + + if ($Database) { + # database is being returned as something weird. change it to string without using a method then trim. + $Database = "$Database" + $Database = $Database.Trim() + $sqlconnection.ChangeDatabase($Database) + } + + $sqlcommand = New-Object System.Data.SqlClient.SqlCommand + $sqlcommand.CommandType = "StoredProcedure" + $sqlcommand.CommandText = "dbo.sp_WhoIsActive" + $sqlcommand.Connection = $sqlconnection + + foreach ($param in $passedparams) { + Write-Message -Level Verbose -Message "Check parameter '$param'" + + $sqlparam = $paramdictionary[$param] + + if ($sqlparam) { + + $value = $localparams[$param] + + switch ($value) { + $true { $value = 1 } + $false { $value = 0 } + } + Write-Message -Level Verbose -Message "Adding parameter '$sqlparam' with value '$value'" + [Void]$sqlcommand.Parameters.AddWithValue($sqlparam, $value) + } + } + + $datatable = New-Object system.Data.DataSet + $dataadapter = New-Object system.Data.SqlClient.SqlDataAdapter($sqlcommand) + $dataadapter.fill($datatable) | Out-Null + $datatable.Tables.Rows + } + catch { + if ($_.Exception.InnerException -Like "*Could not find*") { + Stop-Function -Message "sp_whoisactive not found, please install using Install-DbaWhoIsActive." -Continue + } + else { + Stop-Function -Message "Invalid query." -Continue + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Show-SqlWhoIsActive -CustomMessage "Show-SqlWhoIsActive is no longer supported. Use Invoke-DbaWhoIsActive | Out-GridView for similar results." + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Invoke-DbaXeReplay { + <# + .SYNOPSIS + This command replays events from Read-DbaXEFile on one or more target servers + + .DESCRIPTION + This command replays events from Read-DbaXEFile. It is simplistic in its approach. + + - Writes all queries to a temp sql file + - Executes temp file using . $sqlcmd so that batches are executed properly + - Deletes temp file + + .PARAMETER SqlInstance + Target SQL Server(s) + + .PARAMETER SqlCredential + Used to provide alternative credentials. + + .PARAMETER Database + The initial starting database. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Raw + By dafault, the results of . $sqlcmd are collected, cleaned up and displayed. If you'd like to see all results immeidately, use Raw. + + .PARAMETER InputObject + Accepts the object output of Read-DbaXESession. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017 + + Runs all batch_text for sql_batch_completed against tempdb on sql2017. + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017 -Database planning -Event sql_batch_completed + + Sets the *initial* database to planning then runs only sql_batch_completed against sql2017. + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017, sql2016 + + Runs all batch_text for sql_batch_completed against tempdb on sql2017 and sql2016. + + #> + Param ( + [Parameter(Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PsCredential]$SqlCredential, + [string[]]$Database, + [string[]]$Event = @('sql_batch_completed', 'rcp_completed'), + [Parameter(Mandatory, ValueFromPipeline)] + [object]$InputObject, + [switch]$Raw, + [switch]$EnableException + ) + + begin { + $querycolumns = 'statement', 'batch_text' + $timestamp = (Get-Date -Format yyyyMMddHHmm) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $filename = "$temp\dbatools-replay-$timestamp.sql" + Set-Content $filename -Value $null + + $sqlcmd = "$script:PSModuleRoot\bin\sqlcmd\sqlcmd.exe" + } + process { + if (Test-FunctionInterrupt) { return } + if ($InputObject.Name -notin $Event) { + continue + } + + if ($InputObject.statement) { + if ($InputObject.statement -notmatch "ALTER EVENT SESSION") { + Add-Content -Path $filename -Value $InputObject.statement + Add-Content -Path $filename -Value "GO" + } + } + else { + if ($InputObject.batch_text -notmatch "ALTER EVENT SESSION") { + Add-Content -Path $filename -Value $InputObject.batch_text + Add-Content -Path $filename -Value "GO" + } + } + } + end { + if (Test-FunctionInterrupt) { return } + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance." -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $instance -Continue + } + + + if ($Raw) { + if (Test-Bound -ParameterName SqlCredential) { + . $sqlcmd -S $instance -i $filename -U $SqlCredential.Username -P $SqlCredential.GetNetworkCredential().Password + continue + } + else { + . $sqlcmd -S $instance -i $filename + continue + } + } + + if (Test-Bound -ParameterName SqlCredential) { + $output = . $sqlcmd -S $instance -i $filename -U $SqlCredential.Username -P $SqlCredential.GetNetworkCredential().Password + } + else { + $output = . $sqlcmd -S $instance -i $filename + } + + foreach ($line in $output) { + $newline = $line.Trim() + if ($newline -and $newline -notmatch "------------------------------------------------------------------------------------") { + "$newline" + } + } + } + Remove-Item -Path $filename -ErrorAction Ignore + } +} +function Invoke-Sqlcmd2 { + <# + .SYNOPSIS + Runs a T-SQL script. + + .DESCRIPTION + Runs a T-SQL script. Invoke-Sqlcmd2 runs the whole script and only captures the first selected result set, such as the output of PRINT statements when -verbose parameter is specified. + Parameterized queries are supported. + + Help details below borrowed from Invoke-Sqlcmd + + .PARAMETER ServerInstance + Specifies the SQL Server instance(s) to execute the query against. + + .PARAMETER Database + Specifies the name of the database to execute the query against. If specified, this database will be used in the ConnectionString when establishing the connection to SQL Server. + + If a SQLConnection is provided, the default database for that connection is overridden with this database. + + .PARAMETER Query + Specifies one or more queries to be run. The queries can be Transact-SQL, XQuery statements, or sqlcmd commands. Multiple queries in a single batch may be separated by a semicolon. + + Do not specify the sqlcmd GO separator (or, use the ParseGo parameter). Escape any double quotation marks included in the string. + + Consider using bracketed identifiers such as [MyTable] instead of quoted identifiers such as "MyTable". + + .PARAMETER InputFile + Specifies the full path to a file to be used as the query input to Invoke-Sqlcmd2. The file can contain Transact-SQL statements, XQuery statements, sqlcmd commands and scripting variables. + + .PARAMETER Credential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + SECURITY NOTE: If you use the -Debug switch, the connectionstring including plain text password will be sent to the debug stream. + + .PARAMETER Encrypt + If this switch is enabled, the connection to SQL Server will be made using SSL. + + This requires that the SQL Server has been set up to accept SSL requests. For information regarding setting up SSL on SQL Server, see https://technet.microsoft.com/en-us/library/ms189067(v=sql.105).aspx + + .PARAMETER QueryTimeout + Specifies the number of seconds before the queries time out. + + .PARAMETER ConnectionTimeout + Specifies the number of seconds before Invoke-Sqlcmd2 times out if it cannot successfully connect to an instance of the Database Engine. The timeout value must be an integer between 0 and 65534. If 0 is specified, connection attempts do not time out. + + .PARAMETER As + Specifies output type. Valid options for this parameter are 'DataSet', 'DataTable', 'DataRow', 'PSObject', and 'SingleValue' + + PSObject output introduces overhead but adds flexibility for working with results: http://powershell.org/wp/forums/topic/dealing-with-dbnull/ + + .PARAMETER SqlParameters + Specifies a hashtable of parameters for parameterized SQL queries. http://blog.codinghorror.com/give-me-parameterized-sql-or-give-me-death/ + + Example: + + .PARAMETER AppendServerInstance + If this switch is enabled, the SQL Server instance will be appended to PSObject and DataRow output. + + .PARAMETER ParseGo + If this switch is enabled, "GO" statements will be handled automatically. + Every "GO" will effectively run in a separate query, like if you issued multiple Invoke-SqlCmd2 commands. + "GO"s will be recognized if they are on a single line, as this covers + the 95% of the cases "GO" parsing is needed + Note: + Queries will always target that database, e.g. if you have this Query: + USE DATABASE [dbname] + GO + SELECT * from sys.tables + and you call it via + Invoke-SqlCmd2 -ServerInstance instance -Database msdb -Query ... + you'll get back tables from msdb, not dbname. + + + .PARAMETER SQLConnection + Specifies an existing SQLConnection object to use in connecting to SQL Server. If the connection is closed, an attempt will be made to open it. + + .INPUTS + None + You cannot pipe objects to Invoke-Sqlcmd2 + + .OUTPUTS + As PSObject: System.Management.Automation.PSCustomObject + As DataRow: System.Data.DataRow + As DataTable: System.Data.DataTable + As DataSet: System.Data.DataTableCollectionSystem.Data.DataSet + As SingleValue: Dependent on data type in first column. + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -Query "SELECT login_time AS 'StartTime' FROM sysprocesses WHERE spid = 1" + + Connects to a named instance of the Database Engine on a computer and runs a basic T-SQL query. + + StartTime + ----------- + 2010-08-12 21:21:03.593 + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -InputFile "C:\MyFolder\tsqlscript.sql" | Out-File -filePath "C:\MyFolder\tsqlscript.rpt" + + Reads a file containing T-SQL statements, runs the file, and writes the output to another file. + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -Query "PRINT 'hello world'" -Verbose + + Uses the PowerShell -Verbose parameter to return the message output of the PRINT command. + VERBOSE: hello world + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance MyServer\MyInstance -Query "SELECT ServerName, VCNumCPU FROM tblServerInfo" -as PSObject | ?{$_.VCNumCPU -gt 8} + Invoke-Sqlcmd2 -ServerInstance MyServer\MyInstance -Query "SELECT ServerName, VCNumCPU FROM tblServerInfo" -as PSObject | ?{$_.VCNumCPU} + + This example uses the PSObject output type to allow more flexibility when working with results. + + If we used DataRow rather than PSObject, we would see the following behavior: + Each row where VCNumCPU does not exist would produce an error in the first example + Results would include rows where VCNumCPU has DBNull value in the second example + + .EXAMPLE + 'Instance1', 'Server1/Instance1', 'Server2' | Invoke-Sqlcmd2 -query "Sp_databases" -as psobject -AppendServerInstance + + This example lists databases for each instance. It includes a column for the ServerInstance in question. + DATABASE_NAME DATABASE_SIZE REMARKS ServerInstance + ------------- ------------- ------- -------------- + REDACTED 88320 Instance1 + master 17920 Instance1 + ... + msdb 618112 Server1/Instance1 + tempdb 563200 Server1/Instance1 + ... + OperationsManager 20480000 Server2 + + .EXAMPLE + #Construct a query using SQL parameters + $Query = "SELECT ServerName, VCServerClass, VCServerContact FROM tblServerInfo WHERE VCServerContact LIKE @VCServerContact AND VCServerClass LIKE @VCServerClass" + + #Run the query, specifying values for SQL parameters + Invoke-Sqlcmd2 -ServerInstance SomeServer\NamedInstance -Database ServerDB -query $query -SqlParameters @{ VCServerContact="%cookiemonster%"; VCServerClass="Prod" } + + ServerName VCServerClass VCServerContact + ---------- ------------- --------------- + SomeServer1 Prod cookiemonster, blah + SomeServer2 Prod cookiemonster + SomeServer3 Prod blah, cookiemonster + + .EXAMPLE + Invoke-Sqlcmd2 -SQLConnection $Conn -Query "SELECT login_time AS 'StartTime' FROM sysprocesses WHERE spid = 1" + + Uses an existing SQLConnection and runs a basic T-SQL query against it + + StartTime + ----------- + 2010-08-12 21:21:03.593 + + .EXAMPLE + Invoke-SqlCmd -SQLConnection $Conn -Query "SELECT ServerName FROM tblServerInfo WHERE ServerName LIKE @ServerName" -SqlParameters @{"ServerName = "c-is-hyperv-1"} + + Executes a parameterized query against the existing SQLConnection, with a collection of one parameter to be passed to the query when executed. + + .NOTES + Changelog moved to CHANGELOG.md: + + https://github.com/sqlcollaborative/Invoke-SqlCmd2/blob/master/CHANGELOG.md + + .LINK + https://github.com/sqlcollaborative/Invoke-SqlCmd2 + + .LINK + https://github.com/RamblingCookieMonster/PowerShell + + .FUNCTIONALITY + SQL + #> + + [CmdletBinding(DefaultParameterSetName = 'Ins-Que')] + [OutputType([System.Management.Automation.PSCustomObject], [System.Data.DataRow], [System.Data.DataTable], [System.Data.DataTableCollection], [System.Data.DataSet])] + param ( + [Parameter(ParameterSetName = 'Ins-Que', + Position = 0, + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + HelpMessage = 'SQL Server Instance required...')] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 0, + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + HelpMessage = 'SQL Server Instance required...')] + [Alias('Instance', 'Instances', 'ComputerName', 'Server', 'Servers', 'SqlInstance')] + [ValidateNotNullOrEmpty()] + [string[]]$ServerInstance, + [Parameter(Position = 1, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [string]$Database, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Que', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [string]$Query, + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Fil', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateScript( { Test-Path -LiteralPath $_ })] + [string]$InputFile, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 3, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 3, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Alias('SqlCredential')] + [System.Management.Automation.PSCredential]$Credential, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 4, + Mandatory = $false, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 4, + Mandatory = $false, + ValueFromRemainingArguments = $false)] + [switch]$Encrypt, + [Parameter(Position = 5, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Int32]$QueryTimeout = 600, + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 6, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Que', + Position = 6, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Int32]$ConnectionTimeout = 15, + [Parameter(Position = 7, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateSet("DataSet", "DataTable", "DataRow", "PSObject", "SingleValue")] + [string]$As = "DataRow", + [Parameter(Position = 8, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [System.Collections.IDictionary]$SqlParameters, + [Parameter(Position = 9, + Mandatory = $false)] + [switch]$AppendServerInstance, + [Parameter(Position = 10, + Mandatory = $false)] + [switch]$ParseGO, + [Parameter(ParameterSetName = 'Con-Que', + Position = 11, + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Fil', + Position = 11, + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false, + ValueFromRemainingArguments = $false)] + [Alias('Connection', 'Conn')] + [ValidateNotNullOrEmpty()] + [System.Data.SqlClient.SQLConnection]$SQLConnection + ) + + begin { + if ($InputFile) { + $filePath = $(Resolve-Path -LiteralPath $InputFile).ProviderPath + $Query = [System.IO.File]::ReadAllText("$filePath") + } + + Write-Verbose "Running Invoke-Sqlcmd2 with ParameterSet '$($PSCmdlet.ParameterSetName)'. Performing query '$Query'." + + if ($As -eq "PSObject") { + #This code scrubs DBNulls. Props to Dave Wyatt + $cSharp = @' + using System; + using System.Data; + using System.Management.Automation; + + public class DBNullScrubber + { + public static PSObject DataRowToPSObject(DataRow row) + { + PSObject psObject = new PSObject(); + + if (row != null && (row.RowState & DataRowState.Detached) != DataRowState.Detached) + { + foreach (DataColumn column in row.Table.Columns) + { + Object value = null; + if (!row.IsNull(column)) + { + value = row[column]; + } + + psObject.Properties.Add(new PSNoteProperty(column.ColumnName, value)); + } + } + + return psObject; + } + } +'@ + + try { + Add-Type -TypeDefinition $cSharp -ReferencedAssemblies 'System.Data', 'System.Xml' -ErrorAction stop + } + catch { + if (-not $_.ToString() -like "*The type name 'DBNullScrubber' already exists*") { + Write-Warning "Could not load DBNullScrubber. Defaulting to DataRow output: $_." + $As = "Datarow" + } + } + } + + #Handle existing connections + if ($PSBoundParameters.ContainsKey('SQLConnection')) { + if ($SQLConnection.State -notlike "Open") { + try { + Write-Verbose "Opening connection from '$($SQLConnection.State)' state." + $SQLConnection.Open() + } + catch { + throw $_ + } + } + + if ($Database -and $SQLConnection.Database -notlike $Database) { + try { + Write-Verbose "Changing SQLConnection database from '$($SQLConnection.Database)' to $Database." + $SQLConnection.ChangeDatabase($Database) + } + catch { + throw "Could not change Connection database '$($SQLConnection.Database)' to $Database`: $_" + } + } + + if ($SQLConnection.state -like "Open") { + $ServerInstance = @($SQLConnection.DataSource) + } + else { + throw "SQLConnection is not open" + } + } + $GoSplitterRegex = [regex]'(?smi)^[\s]*GO[\s]*$' + + } + process { + foreach ($SQLInstance in $ServerInstance) { + Write-Verbose "Querying ServerInstance '$SQLInstance'" + + if ($PSBoundParameters.Keys -contains "SQLConnection") { + $Conn = $SQLConnection + } + else { + $CSBuilder = New-Object -TypeName System.Data.SqlClient.SqlConnectionStringBuilder + $CSBuilder["Server"] = $SQLInstance + $CSBuilder["Database"] = $Database + $CSBuilder["Connection Timeout"] = $ConnectionTimeout + + if ($Encrypt) { + $CSBuilder["Encrypt"] = $true + } + + if ($Credential) { + $CSBuilder["Trusted_Connection"] = $false + $CSBuilder["User ID"] = $Credential.UserName + $CSBuilder["Password"] = $Credential.GetNetworkCredential().Password + } + else { + $CSBuilder["Integrated Security"] = $true + } + if ($ApplicationName) { + $CSBuilder["Application Name"] = $ApplicationName + } + else { + $ScriptName = (Get-PSCallStack)[-1].Command.ToString() + if ($ScriptName -ne "") { + $CSBuilder["Application Name"] = $ScriptName + } + } + $conn = New-Object -TypeName System.Data.SqlClient.SQLConnection + + $ConnectionString = $CSBuilder.ToString() + $conn.ConnectionString = $ConnectionString + Write-Debug "ConnectionString $ConnectionString" + + try { + $conn.Open() + } + catch { + Write-Error $_ + continue + } + } + + #Following EventHandler is used for PRINT and RAISERROR T-SQL statements. Executed when -Verbose parameter specified by caller + if ($PSBoundParameters.Verbose) { + $conn.FireInfoMessageEventOnUserErrors = $false # Shiyang, $true will change the SQL exception to information + $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { Write-Verbose "$($_)" } + $conn.add_InfoMessage($handler) + } + if ($ParseGO) { + Write-Verbose "Stripping GOs from source" + $Pieces = $GoSplitterRegex.Split($Query) + } + else { + $Pieces = , $Query + } + # Only execute non-empty statements + $Pieces = $Pieces | Where-Object { $_.Trim().Length -gt 0 } + foreach ($piece in $Pieces) { + $cmd = New-Object system.Data.SqlClient.SqlCommand($piece, $conn) + $cmd.CommandTimeout = $QueryTimeout + + if ($null -ne $SqlParameters) { + $SqlParameters.GetEnumerator() | + ForEach-Object { + if ($null -ne $_.Value) { + $cmd.Parameters.AddWithValue($_.Key, $_.Value) + } + else { + $cmd.Parameters.AddWithValue($_.Key, [DBNull]::Value) + } + } > $null + } + + $ds = New-Object system.Data.DataSet + $da = New-Object system.Data.SqlClient.SqlDataAdapter($cmd) + + try { + [void]$da.fill($ds) + } + catch [System.Data.SqlClient.SqlException] { + # For SQL exception + + $Err = $_ + + Write-Verbose "Capture SQL Error" + + if ($PSBoundParameters.Verbose) { + Write-Verbose "SQL Error: $Err" + } #Shiyang, add the verbose output of exception + + switch ($ErrorActionPreference.tostring()) { + { 'SilentlyContinue', 'Ignore' -contains $_ } { + + } + 'Stop' { + throw $Err + } + 'Continue' { + throw $Err + } + Default { + Throw $Err + } + } + } + catch { + # For other exception + Write-Verbose "Capture Other Error" + + $Err = $_ + + if ($PSBoundParameters.Verbose) { + Write-Verbose "Other Error: $Err" + } + + switch ($ErrorActionPreference.tostring()) { + { 'SilentlyContinue', 'Ignore' -contains $_ } { + + } + 'Stop' { + throw $Err + } + 'Continue' { + throw $Err + } + Default { + throw $Err + } + } + } + finally { + #Close the connection + if (-not $PSBoundParameters.ContainsKey('SQLConnection')) { + $conn.Close() + } + } + + if ($AppendServerInstance) { + #Basics from Chad Miller + $Column = New-Object Data.DataColumn + $Column.ColumnName = "ServerInstance" + + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0].Columns.Add($Column) + Foreach ($row in $ds.Tables[0]) { + $row.ServerInstance = $SQLInstance + } + } + } + + switch ($As) { + 'DataSet' { + $ds + } + 'DataTable' { + $ds.Tables + } + 'DataRow' { + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0] + } + } + 'PSObject' { + if ($ds.Tables.Count -ne 0) { + #Scrub DBNulls - Provides convenient results you can use comparisons with + #Introduces overhead (e.g. ~2000 rows w/ ~80 columns went from .15 Seconds to .65 Seconds - depending on your data could be much more!) + foreach ($row in $ds.Tables[0].Rows) { + + [DBNullScrubber]::DataRowToPSObject($row) + } + } + } + 'SingleValue' { + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0] | Select-Object -ExpandProperty $ds.Tables[0].Columns[0].ColumnName + } + } + } + } + } + } +} #Invoke-Sqlcmd2 +function Measure-DbaBackupThroughput { + <# + .SYNOPSIS + Determines how quickly SQL Server is backing up databases to media. + + .DESCRIPTION + Returns backup history details for one or more databases on a SQL Server. + + Output looks like this: + SqlInstance : sql2016 + Database : SharePoint_Config + AvgThroughputMB : 1.07 + AvgSizeMB : 24.17 + AvgDuration : 00:00:01.1000000 + MinThroughputMB : 0.02 + MaxThroughputMB : 2.26 + MinBackupDate : 8/6/2015 10:22:01 PM + MaxBackupDate : 6/19/2016 12:57:45 PM + BackupCount : 10 + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER Type + By default, this command measures the speed of Full backups. Valid options are "Full", "Log" and "Differential". + + .PARAMETER Since + All backups taken on or after the point in time represented by this datetime object will be processed. + + .PARAMETER Last + If this switch is enabled, only the last backup will be measured. + + .PARAMETER DeviceType + Specifies one or more DeviceTypes to use in filtering backup sets. Valid values are "Disk", "Permanent Disk Device", "Tape", "Permanent Tape Device", "Pipe", "Permanent Pipe Device" and "Virtual Device", as well as custom integers for your own DeviceTypes. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup, Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Measure-DbaBackupThroughput + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 + + Parses every backup in msdb's backuphistory for stats on all databases. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Database AdventureWorks2014 + + Parses every backup in msdb's backuphistory for stats on AdventureWorks2014. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2005 -Last + + Processes the last full, diff and log backups every backup for all databases on sql2005. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2005 -Last -Type Log + + Processes the last log backups every backup for all databases on sql2005. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-7) + + Gets backup calculations for the last week. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-365) -Database bigoldb + + Gets backup calculations, limited to the last year and only the bigoldb database + + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "Instance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$Since, + [switch]$Last, + [ValidateSet("Full", "Log", "Differential", "File", "Differential File", "Partial Full", "Partial Differential")] + [string]$Type = "Full", + [string[]]$DeviceType, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $DatabaseCollection = $server.Databases | Where-Object Name -in $Database + } + else { + $DatabaseCollection = $server.Databases + } + + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $DatabaseCollection) { + Write-Message -Level VeryVerbose -Message "Retrieving history for $db." + $allhistory = @() + + # Splatting didn't work + if ($since) { + $histories = Get-DbaBackupHistory -SqlInstance $server -Database $db.name -Since $since -DeviceType $DeviceType -Type $Type + } + else { + $histories = Get-DbaBackupHistory -SqlInstance $server -Database $db.name -Last:$last -DeviceType $DeviceType -Type $Type + } + + foreach ($history in $histories) { + $timetaken = New-TimeSpan -Start $history.Start -End $history.End + + if ($timetaken.TotalMilliseconds -eq 0) { + $throughput = $history.TotalSize.Megabyte + } + else { + $throughput = $history.TotalSize.Megabyte / $timetaken.TotalSeconds + } + + Add-Member -Force -InputObject $history -MemberType Noteproperty -Name MBps -value $throughput + + $allhistory += $history | Select-Object ComputerName, InstanceName, SqlInstance, Database, MBps, TotalSize, Start, End + } + + Write-Message -Level VeryVerbose -Message "Calculating averages for $db." + foreach ($db in ($allhistory | Sort-Object Database | Group-Object Database)) { + + $measuremb = $db.Group.MBps | Measure-Object -Average -Minimum -Maximum + $measurestart = $db.Group.Start | Measure-Object -Minimum + $measureend = $db.Group.End | Measure-Object -Maximum + $measuresize = $db.Group.TotalSize.Megabyte | Measure-Object -Average + $avgduration = $db.Group | ForEach-Object { New-TimeSpan -Start $_.Start -End $_.End } | Measure-Object -Average TotalSeconds + + [pscustomobject]@{ + ComputerName = $db.Group.ComputerName | Select-Object -First 1 + InstanceName = $db.Group.InstanceName | Select-Object -First 1 + SqlInstance = $db.Group.SqlInstance | Select-Object -First 1 + Database = $db.Name + AvgThroughputMB = [System.Math]::Round($measuremb.Average, 2) + AvgSizeMB = [System.Math]::Round($measuresize.Average, 2) + AvgDuration = [dbatimespan](New-TimeSpan -Seconds $avgduration.Average) + MinThroughputMB = [System.Math]::Round($measuremb.Minimum, 2) + MaxThroughputMB = [System.Math]::Round($measuremb.Maximum, 2) + MinBackupDate = [dbadatetime]$measurestart.Minimum + MaxBackupDate = [dbadatetime]$measureend.Maximum + BackupCount = $db.Count + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName + } + } + } + } +} +function Measure-DbaDiskSpaceRequirement { + <# + .SYNOPSIS + Calculate the space needed to copy and possibly replace a database from one SQL server to another. + + .DESCRIPTION + Returns a file list from source and destination where source file may overwrite destination. Complex scenarios where a new file may exist is taken into account. + This command will accept a hash object in pipeline with the following keys: Source, SourceDatabase, Destination. Using this command will provide a way to prepare before a complex migration with multiple databases from different sources and destinations. + + .PARAMETER Source + Source SQL Server. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to copy. It MUST exist. + + .PARAMETER Destination + Destination SQL Server instance. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationDatabase + The database name at destination. + May or may not be present, if unspecified it will default to the database name provided in SourceDatabase. + + .PARAMETER Credential + The credentials to use to connect via CIM/WMI/PowerShell remoting. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, DiskSpace, Migration + Author: Pollus Brodeur (@pollusb) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Measure-DbaDiskSpaceRequirement + + .EXAMPLE + Measure-DbaDiskSpaceRequirement -Source INSTANCE1 -Database DB1 -Destination INSTANCE2 + + Calculate space needed for a simple migration with one database with the same name at destination. + + .EXAMPLE + @([PSCustomObject]@{Source='SQL1';Destination='SQL2';Database='DB1'}, + [PSCustomObject]@{Source='SQL1';Destination='SQL2';Database='DB2'} + ) | Measure-DbaDiskSpaceRequirement + + Using a PSCustomObject with 2 databases to migrate on SQL2. + + .EXAMPLE + Import-Csv -Path .\migration.csv -Delimiter "`t" | Measure-DbaDiskSpaceRequirement | Format-Table -AutoSize + + Using a CSV file. You will need to use this header line "SourceDestinationDatabaseDestinationDatabase". + + .EXAMPLE + Invoke-DbaSqlCmd -SqlInstance DBA -Database Migrations -Query 'select Source, Destination, Database from dbo.Migrations' ` + | Measure-DbaDiskSpaceRequirement + + Using a SQL table. We are DBA after all! + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [DbaInstanceParameter]$Source, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string]$Database, + [PSCredential]$SourceSqlCredential, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [DbaInstanceParameter]$Destination, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [string]$DestinationDatabase, + [PSCredential]$DestinationSqlCredential, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $local:cacheMP = @{} + $local:cacheDP = @{} + + function Get-MountPointFromPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $path, + [Parameter(Mandatory = $true)] + $computerName, + [PSCredential]$credential + ) + if (!$cacheMP[$computerName]) { + try { + $cacheMP.Add($computerName, (Get-DbaDiskSpace -ComputerName $computerName -Credential $Credential -EnableException)) + Write-Message -Level Verbose -Message "cacheMP[$computerName] is in cache" + } + catch { + # This way, I won't be asking again for this computer. + $cacheMP.Add($computerName, '?') + Stop-Function -Message "Can't connect to $computerName. cacheMP[$computerName] = ?" -ErrorRecord $_ -Target $computerName -Continue + } + } + if ($cacheMP[$computerName] -eq '?') { + return '?' + } + foreach ($m in ($cacheMP[$computerName] | Sort-Object -Property Name -Descending)) { + if ($path -like "$($m.Name)*") { + return $m.Name + } + } + Write-Message -Level Warning -Message "Path $path can't be found in any MountPoints of $computerName" + } + function Get-MountPointFromDefaultPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Log', 'Data')] + $DefaultPathType, + [Parameter(Mandatory = $true)] + $SqlInstance, + [PSCredential]$SqlCredential, + # Could probably use the computer defined in SqlInstance but info was already available from the caller + $computerName, + [PSCredential]$Credential + ) + if (!$cacheDP[$SqlInstance]) { + try { + $cacheDP.Add($SqlInstance, (Get-DbaDefaultPath -SqlInstance $SqlInstance -SqlCredential $SqlCredential -EnableException)) + Write-Message -Level Verbose -Message "cacheDP[$SqlInstance] is in cache" + } + catch { + Stop-Function -Message "Can't connect to $SqlInstance" -Continue + $cacheDP.Add($SqlInstance, '?') + return '?' + } + } + if ($cacheDP[$SqlInstance] -eq '?') { + return '?' + } + if (!$computerName) { + $computerName = $cacheDP[$SqlInstance].ComputerName + } + if (!$cacheMP[$computerName]) { + try { + $cacheMP.Add($computerName, (Get-DbaDiskSpace -ComputerName $computerName -Credential $Credential)) + } + catch { + Stop-Function -Message "Can't connect to $computerName." -Continue + $cacheMP.Add($computerName, '?') + return '?' + } + } + if ($DefaultPathType -eq 'Log') { + $path = $cacheDP[$SqlInstance].Log + } + else { + $path = $cacheDP[$SqlInstance].Data + } + foreach ($m in ($cacheMP[$computerName] | Sort-Object -Property Name -Descending)) { + if ($path -like "$($m.Name)*") { + return $m.Name + } + } + } + } + process { + Write-Message -Level Verbose -Message "Attempting to connect to SQL Servers." + try { + Write-Message -Level Verbose -Message "Connecting to $Source." + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $Source + } + + try { + Write-Message -Level Verbose -Message "Connecting to $Destination." + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $Destination + } + + if (Test-Bound 'DestinationDatabase' -not) { + $DestinationDatabase = $Database + } + Write-Message -Level Verbose -Message "$Source.[$Database] -> $Destination.[$DestinationDatabase]" + + $sourceDb = Get-DbaDatabase -SqlInstance $sourceServer -Database $Database -SqlCredential $SourceSqlCredential + if (Test-Bound 'Database' -not) { + Stop-Function -Message "Database [$Database] MUST exist on Source Instance $Source." -ErrorRecord $_ + } + $sourceFiles = @($sourceDb.FileGroups.Files | Select-Object Name, FileName, Size, @{n='Type'; e= {'Data'}}) + $sourceFiles += @($sourceDb.LogFiles | Select-Object Name, FileName, Size, @{n='Type'; e= {'Log'}}) + + if ($destDb = Get-DbaDatabase -SqlInstance $destServer -Database $DestinationDatabase -SqlCredential $DestinationSqlCredential) { + $destFiles = @($destDb.FileGroups.Files | Select-Object Name, FileName, Size, @{n='Type'; e= {'Data'}}) + $destFiles += @($destDb.LogFiles | Select-Object Name, FileName, Size, @{n='Type'; e= {'Log'}}) + $computerName = $destDb.ComputerName + } + else { + Write-Message -Level Verbose -Message "Database [$DestinationDatabase] does not exist on Destination Instance $Destination." + $computerName = $destServer.NetName + } + + foreach ($sourceFile in $sourceFiles) { + foreach ($destFile in $destFiles) { + if ($found = ($sourceFile.Name -eq $destFile.Name)) { + # Files found on both sides + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabase = $sourceDb.Name + SourceLogicalName = $sourceFile.Name + SourceFileName = $sourceFile.FileName + SourceFileSize = [DbaSize]($sourceFile.Size * 1000) + DestinationDatabase = $destDb.Name + DestinationLogicalName = $destFile.Name + DestinationFileName = $destFile.FileName + DestinationFileSize = [DbaSize]($destFile.Size * 1000) * -1 + DifferenceSize = [DbaSize]( ($sourceFile.Size * 1000) - ($destFile.Size * 1000) ) + MountPoint = Get-MountPointFromPath -Path $destFile.Filename -ComputerName $computerName -Credential $Credential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + break + } + } + if (!$found) { + # Files on source but not on destination + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabase = $sourceDb.Name + SourceLogicalName = $sourceFile.Name + SourceFileName = $sourceFile.FileName + SourceFileSize = [DbaSize]($sourceFile.Size * 1000) + DestinationDatabase = $DestinationDatabase + DestinationLogicalName = $null + DestinationFileName = $null + DestinationFileSize = [DbaSize]0 + DifferenceSize = [DbaSize]($sourceFile.Size * 1000) + MountPoint = Get-MountPointFromDefaultPath -DefaultPathType $sourceFile.Type -SqlInstance $destServer -SqlCredential $DestinationSqlCredential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + } + } + if ($destDb) { + # Files on destination but not on source (strange scenario but possible) + $destFilesNotSource = Compare-Object -ReferenceObject $destFiles -DifferenceObject $sourceFiles -Property Name -PassThru + foreach ($destFileNotSource in $destFilesNotSource) { + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabaseName = $Database + SourceLogicalName = $null + SourceFileName = $null + SourceFileSize = [DbaSize]0 + DestinationDatabaseName = $destDb.Name + DestinationLogicalName = $destFileNotSource.Name + DestinationFileName = $destFile.FileName + DestinationFileSize = [DbaSize]($destFileNotSource.Size * 1000) * -1 + DifferenceSize = [DbaSize]($destFileNotSource.Size * 1000) * -1 + MountPoint = Get-MountPointFromPath -Path $destFileNotSource.Filename -ComputerName $computerName -Credential $Credential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + } + } + $DestinationDatabase = $null + } +} +function Mount-DbaDatabase { + <# + .SYNOPSIS + Attach a SQL Server Database - aliased to Attach-DbaDatabase + + .DESCRIPTION + This command will attach a SQL Server database. + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to attach. + + .PARAMETER FileStructure + A StringCollection object value that contains a list database files. If FileStructure is not specified, BackupHistory will be used to guess the structure. + + .PARAMETER DatabaseOwner + Sets the database owner for the database. The sa account (or equivalent) will be used if DatabaseOwner is not specified. + + .PARAMETER AttachOption + An AttachOptions object value that contains the attachment options. Valid options are "None", "RebuildLog", "EnableBroker", "NewBroker" and "ErrorBrokerConversations". + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Mount-DbaDatabase + + .EXAMPLE + $fileStructure = New-Object System.Collections.Specialized.StringCollection + $fileStructure.Add("E:\archive\example.mdf") + $filestructure.Add("E:\archive\example.ldf") + $filestructure.Add("E:\archive\example.ndf") + Mount-DbaDatabase -SqlInstance sql2016 -Database example -FileStructure $fileStructure + + Attaches a database named "example" to sql2016 with the files "E:\archive\example.mdf", "E:\archive\example.ldf" and "E:\archive\example.ndf". The database owner will be set to sa and the attach option is None. + + .EXAMPLE + Mount-DbaDatabase -SqlInstance sql2016 -Database example + + Since the FileStructure was not provided, this command will attempt to determine it based on backup history. If found, a database named example will be attached to sql2016. + + .EXAMPLE + Mount-DbaDatabase -SqlInstance sql2016 -Database example -WhatIf + + Shows what would happen if the command were executed (without actually performing the command) + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [parameter(Mandatory)] + [string[]]$Database, + [System.Collections.Specialized.StringCollection]$FileStructure, + [string]$DatabaseOwner, + [ValidateSet('None', 'RebuildLog', 'EnableBroker', 'NewBroker', 'ErrorBrokerConversations')] + [string]$AttachOption = "None", + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (-not $server.Logins.Item($DatabaseOwner)) { + try { + $DatabaseOwner = ($server.Logins | Where-Object { $_.id -eq 1 }).Name + } + catch { + $DatabaseOwner = "sa" + } + } + + foreach ($db in $database) { + + if ($server.Databases[$db]) { + Stop-Function -Message "$db is already attached to $server." -Target $db -Continue + } + + if ($server.Databases[$db].IsSystemObject) { + Stop-Function -Message "$db is a system database and cannot be attached using this method." -Target $db -Continue + } + + if (-Not (Test-Bound -Parameter FileStructure)) { + $backuphistory = Get-DbaBackupHistory -SqlInstance $server -Database $db -Type Full | Sort-Object End -Descending | Select-Object -First 1 + + if (-not $backuphistory) { + $message = "Could not enumerate backup history to automatically build FileStructure. Rerun the command and provide the filestructure parameter." + Stop-Function -Message $message -Target $db -Continue + } + + $backupfile = $backuphistory.Path[0] + $filepaths = (Read-DbaBackupHeader -SqlInstance $server -FileList -Path $backupfile).PhysicalName + + $FileStructure = New-Object System.Collections.Specialized.StringCollection + foreach ($file in $filepaths) { + $exists = Test-DbaSqlpath -SqlInstance $server -Path $file + if (-not $exists) { + $message = "Could not find the files to build the FileStructure. Rerun the command and provide the FileStructure parameter." + Stop-Function -Message $message -Target $file -Continue + } + + $null = $FileStructure.Add($file) + } + } + + If ($Pscmdlet.ShouldProcess($server, "Attaching $Database with $DatabaseOwner as database owner and $AttachOption as attachoption")) { + try { + $server.AttachDatabase($db, $FileStructure, $DatabaseOwner, [Microsoft.SqlServer.Management.Smo.AttachOptions]::$AttachOption) + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + AttachResult = "Success" + AttachOption = $AttachOption + FileStructure = $FileStructure + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server + } + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentJob { + <# +.SYNOPSIS +New-DbaAgentJob creates a new job + +.DESCRIPTION +New-DbaAgentJob makes is possible to create a job in the SQL Server Agent. +It returns an array of the job(s) created + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. The name must be unique and cannot contain the percent (%) character. + +.PARAMETER Schedule +Schedule to attach to job. This can be more than one schedule. + +.PARAMETER ScheduleId +Schedule ID to attach to job. This can be more than one schedule ID. + +.PARAMETER Disabled +Sets the status of the job to disabled. By default a job is enabled. + +.PARAMETER Description +The description of the job. + +.PARAMETER StartStepId +The identification number of the first step to execute for the job. + +.PARAMETER Category +The category of the job. + +.PARAMETER OwnerLogin +The name of the login that owns the job. + +.PARAMETER EventLogLevel +Specifies when to place an entry in the Microsoft Windows application log for this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailLevel +Specifies when to send an e-mail upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER NetsendLevel +Specifies when to send a network message upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER PageLevel +Specifies when to send a page upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailOperator +The e-mail name of the operator to whom the e-mail is sent when EmailLevel is reached. + +.PARAMETER NetsendOperator +The name of the operator to whom the network message is sent. + +.PARAMETER PageOperator +The name of the operator to whom a page is sent. + +.PARAMETER DeleteLevel +Specifies when to delete the job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJob + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Description 'Just another job' +Creates a job with the name "Job1" and a small description + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Disabled +Creates the job but sets it to disabled + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -EventLogLevel OnSuccess +Creates the job and sets the notification to write to the Windows Application event log on success + +.EXAMPLE +New-DbaAgentJob -SqlInstance SSTAD-PC -Job 'Job One' -EmailLevel OnFailure -EmailOperator dba +Creates the job and sets the notification to send an e-mail to the e-mail operator + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Description 'Just another job' -Whatif +Doesn't create the job but shows what would happen. + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job 'Job One' +Creates a job with the name "Job One" on multiple servers + +.EXAMPLE +"sql1", "sql2", "sql3" | New-DbaAgentJob -Job 'Job One' +Creates a job with the name "Job One" on multiple servers using the pipe line +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Job, + [object[]]$Schedule, + [int[]]$ScheduleId, + [switch]$Disabled, + [string]$Description, + [int]$StartStepId, + [string]$Category, + [string]$OwnerLogin, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EventLogLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EmailLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [Parameter()] + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$PageLevel, + [string]$EmailOperator, + [string]$NetsendOperator, + [string]$PageOperator, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$DeleteLevel, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + # Check of the event log level is of type string and set the integer value + if ($EventLogLevel -notin 1, 2, 3) { + $EventLogLevel = switch ($EventLogLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the email level is of type string and set the integer value + if ($EmailLevel -notin 1, 2, 3) { + $EmailLevel = switch ($EmailLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the net send level is of type string and set the integer value + if ($NetsendLevel -notin 1, 2, 3) { + $NetsendLevel = switch ($NetsendLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the page level is of type string and set the integer value + if ($PageLevel -notin 1, 2, 3) { + $PageLevel = switch ($PageLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the delete level is of type string and set the integer value + if ($DeleteLevel -notin 1, 2, 3) { + $DeleteLevel = switch ($DeleteLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check the e-mail operator name + if (($EmailLevel -ge 1) -and (-not $EmailOperator)) { + Stop-Function -Message "Please set the e-mail operator when the e-mail level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($NetsendLevel -ge 1) -and (-not $NetsendOperator)) { + Stop-Function -Message "Please set the netsend operator when the netsend level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($PageLevel -ge 1) -and (-not $PageOperator)) { + Stop-Function -Message "Please set the page operator when the page level parameter is set." -Target $sqlinstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Check if the job already exists + if (-not $Force -and ($server.JobServer.Jobs.Name -contains $Job)) { + Stop-Function -Message "Job $Job already exists on $instance" -Target $instance -Continue + } + elseif ($Force -and ($server.JobServer.Jobs.Name -contains $Job)) { + Write-Message -Message "Job $Job already exists on $instance. Removing.." -Level Verbose + + if ($PSCmdlet.ShouldProcess($instance, "Removing the job $Job on $instance")) { + try { + Remove-DbaAgentJob -SqlInstance $instance -Job $Job -EnableException + } + catch { + Stop-Function -Message "Couldn't remove job $Job from $instance" -Target $instance -Continue -ErrorRecord $_ + } + } + + } + + if ($PSCmdlet.ShouldProcess($instance, "Creating the job on $instance")) { + # Create the job object + try { + $currentjob = New-Object Microsoft.SqlServer.Management.Smo.Agent.Job($server.JobServer, $Job) + } + catch { + Stop-Function -Message "Something went wrong creating the job. `n" -Target $Job -Continue -ErrorRecord $_ + } + + #region job options + # Settings the options for the job + if ($Disabled) { + Write-Message -Message "Setting job to disabled" -Level Verbose + $currentjob.IsEnabled = $false + } + else { + Write-Message -Message "Setting job to enabled" -Level Verbose + $currentjob.IsEnabled = $true + } + + if ($Description.Length -ge 1) { + Write-Message -Message "Setting job description" -Level Verbose + $currentjob.Description = $Description + } + + if ($StartStepId -ge 1) { + Write-Message -Message "Setting job start step id" -Level Verbose + $currentjob.StartStepID = $StartStepId + } + + if ($Category.Length -ge 1) { + # Check if the job category exists + if ($Category -notin $server.JobServer.JobCategories.Name) { + if ($Force) { + if ($PSCmdlet.ShouldProcess($instance, "Creating job category on $instance")) { + try { + # Create the category + New-DbaAgentJobCategory -SqlInstance $instance -Category $Category + } + catch { + Stop-Function -Message "Couldn't create job category $Category from $instance" -Target $instance -Continue -ErrorRecord $_ + } + } + } + else { + Stop-Function -Message "Job category $Category doesn't exist on $instance. Use -Force to create it." -Target $instance + return + } + } + else { + Write-Message -Message "Setting job category" -Level Verbose + $currentjob.Category = $Category + } + } + + if ($OwnerLogin.Length -ge 1) { + # Check if the login name is present on the instance + if ($server.Logins.Name -contains $OwnerLogin) { + Write-Message -Message "Setting job owner login name to $OwnerLogin" -Level Verbose + $currentjob.OwnerLoginName = $OwnerLogin + } + else { + Stop-Function -Message "The owner $OwnerLogin does not exist on instance $instance" -Target $Job -Continue + } + } + + if ($EventLogLevel -ge 0) { + Write-Message -Message "Setting job event log level" -Level Verbose + $currentjob.EventLogLevel = $EventLogLevel + } + + if ($EmailOperator) { + if ($EmailLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $EmailOperator) { + Write-Message -Message "Setting job e-mail level" -Level Verbose + $currentjob.EmailLevel = $EmailLevel + + Write-Message -Message "Setting job e-mail operator" -Level Verbose + $currentjob.OperatorToEmail = $EmailOperator + } + else { + Stop-Function -Message "The e-mail operator name $EmailOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Stop-Function -Message "Invalid combination of e-mail operator name $EmailOperator and email level $EmailLevel. Not setting the notification." -Target $Job -Continue + } + } + + if ($NetsendOperator) { + if ($NetsendLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $NetsendOperator) { + Write-Message -Message "Setting job netsend level" -Level Verbose + $currentjob.NetSendLevel = $NetsendLevel + + Write-Message -Message "Setting job netsend operator" -Level Verbose + $currentjob.OperatorToNetSend = $NetsendOperator + } + else { + Stop-Function -Message "The netsend operator name $NetsendOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Write-Message -Message "Invalid combination of netsend operator name $NetsendOperator and netsend level $NetsendLevel. Not setting the notification." + } + } + + if ($PageOperator) { + if ($PageLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $PageOperator) { + Write-Message -Message "Setting job pager level" -Level Verbose + $currentjob.PageLevel = $PageLevel + + Write-Message -Message "Setting job pager operator" -Level Verbose + $currentjob.OperatorToPage = $PageOperator + } + else { + Stop-Function -Message "The page operator name $PageOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Write-Message -Message "Invalid combination of page operator name $PageOperator and page level $PageLevel. Not setting the notification." -Level Warning + } + } + + if ($DeleteLevel -ge 0) { + Write-Message -Message "Setting job delete level" -Level Verbose + $currentjob.DeleteLevel = $DeleteLevel + } + #endregion job options + + try { + Write-Message -Message "Creating the job" -Level Verbose + + # Create the job + $currentjob.Create() + + Write-Message -Message "Job created with UID $($currentjob.JobID)" -Level Verbose + + # Make sure the target is set for the job + Write-Message -Message "Applying the target (local) to job $Job" -Level Verbose + $currentjob.ApplyToTargetServer("(local)") + + # If a schedule needs to be attached + if ($Schedule) { + Set-DbaAgentJob -SqlInstance $instance -Job $currentjob -Schedule $Schedule -SqlCredential $SqlCredential + } + + if ($ScheduleId) { + Set-DbaAgentJob -SqlInstance $instance -Job $currentjob -ScheduleId $ScheduleId -SqlCredential $SqlCredential + } + } + catch { + Stop-Function -Message "Something went wrong creating the job" -Target $currentjob -ErrorRecord $_ -Continue + } + } + + # Return the job + return $currentjob + } + } + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job(s)." -Level Verbose + } + +} +function New-DbaAgentJobCategory { + <# +.SYNOPSIS +New-DbaAgentJobCategory creates a new job category. + +.DESCRIPTION +New-DbaAgentJobCategory makes it possible to create a job category that can be used with jobs. +It returns an array of the job(s) created . + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER CategoryType +The type of category. This can be "LocalJob", "MultiServerJob" or "None". +The default is "LocalJob" and will automatically be set when no option is chosen. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJobCategory + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' + +Creates a new job category with the name 'Category 1'. + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 2' -CategoryType MultiServerJob + +Creates a new job category with the name 'Category 2' and assign the category type for a multi server job. + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [ValidateSet("LocalJob", "MultiServerJob", "None")] + [string]$CategoryType, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Check the category type + if (-not $CategoryType) { + # Setting category type to default + Write-Message -Message "Setting the category type to 'LocalJob'" -Level Verbose + $CategoryType = "LocalJob" + } + } + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($cat in $Category) { + # Check if the category already exists + if ($cat -in $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat already exists on $instance" -Target $instance -Continue + } + else { + if ($PSCmdlet.ShouldProcess($instance, "Adding the job category $cat")) { + try { + $jobcategory = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobCategory($server.JobServer, $cat) + $jobcategory.CategoryType = $CategoryType + + $jobcategory.Create() + + $server.JobServer.Refresh() + } + catch { + Stop-Function -Message "Something went wrong creating the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } # if should process + + } # end else category exists + + # Return the job category + Get-DbaAgentJobCategory -SqlInstance $instance -Category $cat + + } # for each category + + } # for each instance + } + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job category." -Level Verbose + } + +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentJobStep { + <# +.SYNOPSIS +New-DbaAgentJobStep creates a new job step for a job + +.DESCRIPTION +New-DbaAgentJobStep creates a new job in the SQL Server Agent for a specific job + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job to which to add the step. + +.PARAMETER StepId +The sequence identification number for the job step. Step identification numbers start at 1 and increment without gaps. + +.PARAMETER StepName +The name of the step. + +.PARAMETER SubSystem +The subsystem used by the SQL Server Agent service to execute command. +Allowed values 'ActiveScripting','AnalysisCommand','AnalysisQuery','CmdExec','Distribution','LogReader','Merge','PowerShell','QueueReader','Snapshot','Ssis','TransactSql' +The default is 'TransactSql' + +.PARAMETER Command +The commands to be executed by SQLServerAgent service through subsystem. + +.PARAMETER CmdExecSuccessCode +The value returned by a CmdExec subsystem command to indicate that command executed successfully. + +.PARAMETER OnSuccessAction +The action to perform if the step succeeds. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnSuccessStepId +The ID of the step in this job to execute if the step succeeds and OnSuccessAction is "GoToStep". + +.PARAMETER OnFailAction +The action to perform if the step fails. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnFailStepId +The ID of the step in this job to execute if the step fails and OnFailAction is "GoToNextStep". + +.PARAMETER Database +The name of the database in which to execute a Transact-SQL step. The default is 'master'. + +.PARAMETER DatabaseUser +The name of the user account to use when executing a Transact-SQL step. + +.PARAMETER RetryAttempts +The number of retry attempts to use if this step fails. The default is 0. + +.PARAMETER RetryInterval +The amount of time in minutes between retry attempts. The default is 0. + +.PARAMETER OutputFileName +The name of the file in which the output of this step is saved. + +.PARAMETER Flag +Sets the flag(s) for the job step. + +Flag Description +---------------------------------------------------------------------------- +AppendAllCmdExecOutputToJobHistory Job history, including command output, is appended to the job history file. +AppendToJobHistory Job history is appended to the job history file. +AppendToLogFile Job history is appended to the SQL Server log file. +AppendToTableLog Job history is appended to a log table. +LogToTableWithOverwrite Job history is written to a log table, overwriting previous contents. +None Job history is not appended to a file. +ProvideStopProcessEvent Job processing is stopped. + +.PARAMETER ProxyName +The name of the proxy that the job step runs as. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Agent, Job, JobStep +Author: Sander Stad (@sqlstad, sqlstad.nl) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJobStep + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 +Create a step in "Job1" with the name Step1 with the default subsystem TransactSql. + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the msdb + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1, Job2, 'Job Three' -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers for multiple jobs + +.EXAMPLE +sql1, sql2, sql3 | New-DbaAgentJobStep -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers using pipeline +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $false)] + [int]$StepId, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [Parameter(Mandatory = $false)] + [ValidateSet('ActiveScripting', 'AnalysisCommand', 'AnalysisQuery', 'CmdExec', 'Distribution', 'LogReader', 'Merge', 'PowerShell', 'QueueReader', 'Snapshot', 'Ssis', 'TransactSql')] + [string]$Subsystem = 'TransactSql', + [Parameter(Mandatory = $false)] + [string]$Command, + [Parameter(Mandatory = $false)] + [int]$CmdExecSuccessCode, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnSuccessAction = 'QuitWithSuccess', + [Parameter(Mandatory = $false)] + [int]$OnSuccessStepId = 0, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnFailAction = 'QuitWithFailure', + [Parameter(Mandatory = $false)] + [int]$OnFailStepId, + [Parameter(Mandatory = $false)] + [object]$Database, + [Parameter(Mandatory = $false)] + [string]$DatabaseUser, + [Parameter(Mandatory = $false)] + [int]$RetryAttempts, + [Parameter(Mandatory = $false)] + [int]$RetryInterval, + [Parameter(Mandatory = $false)] + [string]$OutputFileName, + [Parameter(Mandatory = $false)] + [ValidateSet('AppendAllCmdExecOutputToJobHistory', 'AppendToJobHistory', 'AppendToLogFile', 'LogToTableWithOverwrite', 'None', 'ProvideStopProcessEvent')] + [string[]]$Flag, + [Parameter(Mandatory = $false)] + [string]$ProxyName, + [Parameter(Mandatory = $false)] + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Check the parameter on success step id + if (($OnSuccessAction -ne 'GoToStep') -and ($OnSuccessStepId -ge 1)) { + Stop-Function -Message "Parameter OnSuccessStepId can only be used with OnSuccessAction 'GoToStep'." -Target $SqlInstance + return + } + + # Check the parameter on success step id + if (($OnFailAction -ne 'GoToStep') -and ($OnFailStepId -ge 1)) { + Stop-Function -Message "Parameter OnFailStepId can only be used with OnFailAction 'GoToStep'." -Target $SqlInstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + Write-Message -Message "Job $j doesn't exists on $instance" -Warning + } + else { + # Create the job step object + try { + # Get the job + $currentjob = $Server.JobServer.Jobs[$j] + + # Create the job step + $JobStep = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobStep + + # Set the job where the job steps belongs to + $JobStep.Parent = $currentjob + } + catch { + Stop-Function -Message "Something went wrong creating the job step" -Target $instance -ErrorRecord $_ -Continue + } + + #region job step options + # Setting the options for the job step + if ($StepName) { + # Check if the step already exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + $JobStep.Name = $StepName + } + elseif (($Server.JobServer.Jobs[$j].JobSteps.Name -contains $StepName) -and $Force) { + Write-Message -Message "Step $StepName already exists for job. Force is used. Removing existing step" -Level Verbose + + # Remove the job step based on the name + Remove-DbaAgentJobStep -SqlInstance $instance -Job $currentjob -StepName $StepName + + # Set the name job step object + $JobStep.Name = $StepName + } + else { + Stop-Function -Message "The step name $StepName already exists for job $j" -Target $instance -Continue + } + } + elseif ($StepId) { + # Check if the used step id is already in place + if ($Job.JobSteps.ID -notcontains $StepId) { + Write-Message -Message "Setting job step step id to $StepId" -Level Verbose + $JobStep.ID = $StepId + } + elseif (($Job.JobSteps.ID -contains $StepId) -and $Force) { + Write-Message -Message "Step ID $StepId already exists for job. Force is used. Removing existing step" -Level Verbose + + # Remove the existing job step + $StepName = ($Server.JobServer.Jobs['Job2'].JobSteps | Where-Object {$_.ID -eq 1}).Name + Remove-DbaAgentJobStep -SqlInstance $instance -Job $currentjob -StepName $StepName + + # Set the ID job step object + $JobStep.ID = $StepId + } + else { + Stop-Function -Message "The step id $StepId already exists for job $j" -Target $instance -Continue + } + } + else { + # Get the job step count + $JobStep.ID = $Job.JobSteps.Count + 1 + } + + if ($Subsystem) { + Write-Message -Message "Setting job step subsystem to $Subsystem" -Level Verbose + $JobStep.Subsystem = $Subsystem + } + + if ($Command) { + Write-Message -Message "Setting job step command to $Command" -Level Verbose + $JobStep.Command = $Command + } + + if ($CmdExecSuccessCode) { + Write-Message -Message "Setting job step command exec success code to $CmdExecSuccessCode" -Level Verbose + $JobStep.CommandExecutionSuccessCode = $CmdExecSuccessCode + } + + if ($OnSuccessAction) { + Write-Message -Message "Setting job step success action to $OnSuccessAction" -Level Verbose + $JobStep.OnSuccessAction = $OnSuccessAction + } + + if ($OnSuccessStepId) { + Write-Message -Message "Setting job step success step id to $OnSuccessStepId" -Level Verbose + $JobStep.OnSuccessStep = $OnSuccessStepId + } + + if ($OnFailAction) { + Write-Message -Message "Setting job step fail action to $OnFailAction" -Level Verbose + $JobStep.OnFailAction = $OnFailAction + } + + if ($OnFailStepId) { + Write-Message -Message "Setting job step fail step id to $OnFailStepId" -Level Verbose + $JobStep.OnFailStep = $OnFailStepId + } + + if ($Database) { + # Check if the database is present on the server + if ($Server.Databases.Name -contains $Database) { + Write-Message -Message "Setting job step database name to $Database" -Level Verbose + $JobStep.DatabaseName = $Database + } + else { + Stop-Function -Message "The database is not present on instance $instance." -Target $instance -Continue + } + } + + if ($DatabaseUser -and $DatabaseName) { + # Check if the username is present in the database + if ($Server.Databases[$DatabaseName].Users.Name -contains $DatabaseUser) { + + Write-Message -Message "Setting job step database username to $DatabaseUser" -Level Verbose + $JobStep.DatabaseUserName = $DatabaseUser + } + else { + Stop-Function -Message "The database user is not present in the database $DatabaseName on instance $instance." -Target $instance -Continue + } + } + + if ($RetryAttempts) { + Write-Message -Message "Setting job step retry attempts to $RetryAttempts" -Level Verbose + $JobStep.RetryAttempts = $RetryAttempts + } + + if ($RetryInterval) { + Write-Message -Message "Setting job step retry interval to $RetryInterval" -Level Verbose + $JobStep.RetryInterval = $RetryInterval + } + + if ($OutputFileName) { + Write-Message -Message "Setting job step output file name to $OutputFileName" -Level Verbose + $JobStep.OutputFileName = $OutputFileName + } + + if ($ProxyName) { + # Check if the proxy exists + if ($Server.JobServer.ProxyAccounts.Name -contains $ProxyName) { + Write-Message -Message "Setting job step proxy name to $ProxyName" -Level Verbose + $JobStep.ProxyName = $ProxyName + } + else { + Stop-Function -Message "The proxy name $ProxyName doesn't exist on instance $instance." -Target $instance -Continue + } + } + + if ($Flag.Count -ge 1) { + Write-Message -Message "Setting job step flag(s) to $($Flags -join ',')" -Level Verbose + $JobStep.JobStepFlags = $Flag + } + #endregion job step options + + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Creating the job step $StepName")) { + try { + Write-Message -Message "Creating the job step" -Level Verbose + + # Create the job step + $JobStep.Create() + $currentjob.Alter() + } + catch { + Stop-Function -Message "Something went wrong creating the job step" -Target $instance -ErrorRecord $_ -Continue + } + } + + # Return the job step + $JobStep + } + } # foreach object job + } # foreach object instance + } # process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job step(s)" -Level Verbose + } +} + +function New-DbaAgentProxy { + <# + .SYNOPSIS + Adds one or more proxies to SQL Server Agent + + .DESCRIPTION + Adds one or more proxies to SQL Server Agent + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The name of the proxy or proxies you want to create + + .PARAMETER Credential + The associated SQL Server Credential. The credential must be created prior to creating the Proxy. + + .PARAMETER SubSystem + The associated subsystem or subsystems. Defaults to CmdExec. + + Valid options include: + ActiveScripting + AnalysisCommand + AnalysisQuery + CmdExec + Distribution + LogReader + Merge + PowerShell + QueueReader + Snapshot + Ssis + TransactSql + + .PARAMETER Description + A description of the proxy + + .PARAMETER Login + The SQL Server login or logins (known as proxy principals) to assign to the proxy + + .PARAMETER ServerRole + The SQL Server role or roles (known as proxy principals) to assign to the proxy + + .PARAMETER MsdbRole + The msdb role or roles (known as proxy principals) to assign to the proxy + + .PARAMETER Disabled + Create the proxy as disabled + + .PARAMETER Force + Drop and recreate the proxy if it already exists + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Proxy + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaAgentProxy + + .EXAMPLE + New-DbaAgentProxy -SqlInstance sql2016 -Name STIG -Credential 'PowerShell Proxy' + + Creates an Agent Proxy on sql2016 with the name STIG with the 'PowerShell Proxy' credential. + The proxy is automatically added to the CmdExec subsystem. + + .EXAMPLE + New-DbaAgentProxy -SqlInstance localhost\sql2016 -Name STIG -Credential 'PowerShell Proxy' -Description "Used for auditing purposes" -Login ad\sqlstig -SubSystem CmdExec, PowerShell -ServerRole securtyadmin -MsdbRole ServerGroupAdministratorRole + + Creates an Agent Proxy on sql2016 with the name STIG with the 'PowerShell Proxy' credential and the following principals: + + Login: ad\sqlstig + ServerRole: securtyadmin + MsdbRole: ServerGroupAdministratorRole + + By default, only sysadmins have access to create job steps with proxies. This will allow 3 additional principals access: + The proxy is then added to the CmdExec and PowerShell subsystems + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string[]]$Name, + [parameter(Mandatory)] + [string[]]$Credential, + [ValidateSet("ActiveScripting", "AnalysisCommand", "AnalysisQuery", "CmdExec", "Distribution", "LogReader", "Merge", "PowerShell", "QueueReader", "Snapshot", "Ssis", "TransactSql")] + [string[]]$SubSystem = "CmdExec", + [string]$Description, + [string[]]$Login, + [string[]]$ServerRole, + [string[]]$MsdbRole, + [switch]$Disabled, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $jobServer = $server.JobServer + } + catch { + Stop-Function -Message "Failure. Is SQL Agent started?" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($proxyname in $name) { + + if ($jobServer.ProxyAccounts[$proxyName]) { + if ($force) { + if ($Pscmdlet.ShouldProcess($instance, "Dropping $proxyname")) { + $jobServer.ProxyAccounts[$proxyName].Drop() + $jobServer.ProxyAccounts.Refresh() + } + } + else { + Write-Message -Level Warning -Message "Proxy account $proxy already exists on $instance. Use -Force to drop and recreate." + continue + } + } + + if (-not $server.Credentials[$Credential]) { + Write-Message -Level Warning -Message "Credential '$Credential' does not exist on $instance" + continue + } + + if ($Pscmdlet.ShouldProcess($instance, "Adding $proxyname with the $Credential credential")) { + # the new-object is stubborn and $true/$false has to be forced in + $enabled = switch ($disabled) { + $false { + $proxy = New-Object Microsoft.SqlServer.Management.Smo.Agent.ProxyAccount -ArgumentList $jobServer, $ProxyName, $Credential, $true, $Description + } + $true { + $proxy = New-Object Microsoft.SqlServer.Management.Smo.Agent.ProxyAccount -ArgumentList $jobServer, $ProxyName, $Credential, $false, $Description + } + } + + try { + $proxy.Create() + } + catch { + Stop-Function -Message "Could not create proxy account" -ErrorRecord $_ -Target $instance -Continue + } + } + + foreach ($loginname in $login) { + if ($server.Logins[$loginname]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding login $loginname to proxy")) { + $proxy.AddLogin($loginname) + } + } + else { + Write-Message -Level Warning -Message "Login '$loginname' does not exist on $instance" + } + } + + foreach ($role in $ServerRole) { + if ($server.Roles[$role]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding server role $role to proxy")) { + $proxy.AddServerRole($role) + } + } + else { + Write-Message -Level Warning -Message "Server Role '$role' does not exist on $instance" + } + } + + foreach ($role in $MsdbRole) { + if ($server.Databases['msdb'].Roles[$role]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding msdb role $role to proxy")) { + $proxy.AddMsdbRole($role) + } + } + else { + Write-Message -Level Warning -Message "msdb role '$role' does not exist on $instance" + } + } + + foreach ($system in $SubSystem) { + if ($Pscmdlet.ShouldProcess($instance, "Adding subsystem $system to proxy")) { + $proxy.AddSubSystem($system) + } + } + + if ($Pscmdlet.ShouldProcess("console", "Outputting Proxy object")) { + $proxy.Alter() + $proxy.Refresh() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name Logins -value $proxy.EnumLogins() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name ServerRoles -value $proxy.EnumServerRoles() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name MsdbRoles -value $proxy.EnumMsdbRoles() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name Subsystems -value $proxy.EnumSubSystems() + + Select-DefaultView -InputObject $proxy -Property ComputerName, InstanceName, SqlInstance, ID, Name, CredentialName, CredentialIdentity, Description, Logins, ServerRoles, MsdbRoles, SubSystems, IsEnabled + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentSchedule { + <# + .SYNOPSIS + New-DbaAgentSchedule creates a new schedule in the msdb database. + + .DESCRIPTION + New-DbaAgentSchedule will help create a new schedule for a job. + If the job parameter is not supplied the schedule will not be attached to a job. + + .PARAMETER SqlInstance + SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job that has the schedule. + + .PARAMETER Schedule + The name of the schedule. + + .PARAMETER Disabled + Set the schedule to disabled. Default is enabled + + .PARAMETER FrequencyType + A value indicating when a job is to be executed. + + Allowed values: Once, Daily, Weekly, Monthly, MonthlyRelative, AgentStart or IdleComputer + + If force is used the default will be "Once". + + .PARAMETER FrequencyInterval + The days that a job is executed + + Allowed values: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Weekdays, Weekend or EveryDay. + The other allowed values are the numbers 1 to 31 for each day of the month. + + If "Weekdays", "Weekend" or "EveryDay" is used it over writes any other value that has been passed before. + + If force is used the default will be 1. + + .PARAMETER FrequencySubdayType + Specifies the units for the subday FrequencyInterval. + + Allowed values: Time, Seconds, Minutes, or Hours + + .PARAMETER FrequencySubdayInterval + The number of subday type periods to occur between each execution of a job. + + .PARAMETER FrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + Allowed values: First, Second, Third, Fourth or Last + + .PARAMETER FrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. + + FrequencyRecurrenceFactor is used only if FrequencyType is "Weekly", "Monthly" or "MonthlyRelative". + + .PARAMETER StartDate + The date on which execution of a job can begin. + + If force is used the start date will be the current day + + .PARAMETER EndDate + The date on which execution of a job can stop. + + If force is used the end date will be '9999-12-31' + + .PARAMETER StartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + If force is used the start time will be '00:00:00' + + .PARAMETER EndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + If force is used the start time will be '23:59:59' + + .PARAMETER Owner + The name of the server principal that owns the schedule. If no value is given the schedule is owned by the creator. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + The force parameter will ignore some errors in the parameters and assume defaults. + It will also remove the any present schedules with the same name for the specific job. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, JobStep + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaAgentSchedule + + .EXAMPLE + New-DbaAgentSchedule -SqlInstance localhost\SQL2016 -Schedule daily -FrequencyType Daily -FrequencyInterval Everyday -Force + + Creates a schedule with a daily frequency every day. It assumes default values for the start date, start time, end date and end time due to -Force. + + .EXAMPLE + New-DbaAgentSchedule -SqlInstance sstad-pc -Schedule MonthlyTest -FrequencyType Monthly -FrequencyInterval 10 -FrequencyRecurrenceFactor 1 -Force + + Create a schedule with a monhtly frequency occuring every 10th of the month. It assumes default values for the start date, start time, end date and end time due to -Force. + + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential] + $SqlCredential, + [object[]]$Job, + [object]$Schedule, + [switch]$Disabled, + [ValidateSet('Once', 'Daily', 'Weekly', 'Monthly', 'MonthlyRelative', 'AgentStart', 'IdleComputer')] + [object]$FrequencyType, + [ValidateSet('EveryDay', 'Weekdays', 'Weekend', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31)] + [object[]]$FrequencyInterval, + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$FrequencySubdayType, + [int]$FrequencySubdayInterval, + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$FrequencyRelativeInterval, + [int]$FrequencyRecurrenceFactor, + [string]$StartDate, + [string]$EndDate, + [string]$StartTime, + [string]$EndTime, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # if a Schedule is not provided there is no much point + if (!$Schedule) { + Stop-Function -Message "A schedule was not provided! Please provide a schedule name." + return + } + + [int]$Interval = 0 + + # Translate FrequencyType value from string to the integer value + if (!$FrequencyType -or $FrequencyType) { + [int]$FrequencyType = + switch ($FrequencyType) { + "Once" { 1 } + "Daily" { 4 } + "Weekly" { 8 } + "Monthly" { 16 } + "MonthlyRelative" { 32 } + "AgentStart" { 64 } + "IdleComputer" { 128 } + default { 1 } + } + } + + # Translate FrequencySubdayType value from string to the integer value + if (!$FrequencySubdayType -or $FrequencySubdayType) { + [int]$FrequencySubdayType = + switch ($FrequencySubdayType) { + "Time" { 1 } + "Seconds" { 2 } + "Minutes" { 4 } + "Hours" { 8 } + default { 1 } + } + } + + # Check of the relative FrequencyInterval value is of type string and set the integer value + [int]$FrequencyRelativeInterval = + switch ($FrequencyRelativeInterval) { + "First" { 1 } + "Second" { 2 } + "Third" { 4 } + "Fourth" { 8 } + "Last" { 16 } + "Unused" { 0 } + default {0} + } + + # Check if the interval is valid + if (($FrequencyType -in 4, "Daily") -and (($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365) -and -not $FrequencyInterval -eq "EveryDay")) { + Stop-Function -Message "The frequency interval $FrequencyInterval requires a frequency interval to be between 1 and 365." -Target $SqlInstance + return + } + + # Check if the recurrence factor is set for weekly or monthly interval + if (($FrequencyType -in (16, 8)) -and $FrequencyRecurrenceFactor -lt 1) { + if ($Force) { + $FrequencyRecurrenceFactor = 1 + Write-Message -Message "Recurrence factor not set for weekly or monthly interval. Setting it to $FrequencyRecurrenceFactor." -Level Verbose + } + else { + Stop-Function -Message "The recurrence factor $FrequencyRecurrenceFactor (parameter FrequencyRecurrenceFactor) needs to be at least one when using a weekly or monthly interval." -Target $SqlInstance + return + } + } + + # Check the subday interval + if (($FrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($FrequencySubdayInterval -ge 1 -or $FrequencySubdayInterval -le 59))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 59 when subday type is 'Seconds' or 'Minutes'" -Target $SqlInstance + return + } + elseif (($FrequencySubdayType -eq 8, "Hours") -and (-not ($FrequencySubdayInterval -ge 1 -and $FrequencySubdayInterval -le 23))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 23 when subday type is 'Hours'" -Target $SqlInstance + return + } + + # If the FrequencyInterval is set for the daily FrequencyType + if ($FrequencyType -in 4, 'Daily') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Create the interval to hold the value(s) + switch ($FrequencyInterval) { + "EveryDay" { $Interval = 1} + default {$Interval = 1 } + } + + } + + # If the FrequencyInterval is set for the weekly FrequencyType + if ($FrequencyType -in 8, 'Weekly') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 4 } + "Wednesday" { $Interval += 8 } + "Thursday" { $Interval += 16 } + "Friday" { $Interval += 32 } + "Saturday" { $Interval += 64 } + "Weekdays" { $Interval = 62 } + "Weekend" { $Interval = 65 } + "EveryDay" {$Interval = 127 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 4 { $Interval += 4 } + 8 { $Interval += 8 } + 16 { $Interval += 16 } + 32 { $Interval += 32 } + 64 { $Interval += 64 } + 62 { $Interval = 62 } + 65 { $Interval = 65 } + 127 {$Interval = 127 } + default { $Interval = 0 } + } + } + } + + # If the FrequencyInterval is set for the monthly FrequencyInterval + if ($FrequencyType -in 16, 'Monthly') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + $FrequencyInterval + switch ($Item) { + {[int]$_ -ge 1 -and [int]$_ -le 31} { $Interval = [int]$Item } + } + } + + + } + + # If the FrequencyInterval is set for the relative monthly FrequencyInterval + if ($FrequencyType -eq 32) { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 3 } + "Wednesday" { $Interval += 4 } + "Thursday" { $Interval += 5 } + "Friday" { $Interval += 6 } + "Saturday" { $Interval += 7 } + "Day" { $Interval += 8 } + "Weekday" { $Interval += 9 } + "WeekendDay" { $Interval += 10 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 3 { $Interval += 3 } + 4 { $Interval += 4 } + 5 { $Interval += 5 } + 6 { $Interval += 6 } + 7 { $Interval += 7 } + 8 { $Interval += 8 } + 9 { $Interval += 9 } + 10 { $Interval += 10 } + } + } + } + + # Check if the interval is valid for the frequency + if ($FrequencyType -eq 0) { + if ($Force) { + Write-Message -Message "Parameter FrequencyType must be set to at least [Once]. Setting it to 'Once'." -Level Warning + $FrequencyType = 1 + } + else { + Stop-Function -Message "Parameter FrequencyType must be set to at least [Once]" -Target $SqlInstance + return + } + } + + # Check if the interval is valid for the frequency + if (($FrequencyType -in 4, 8, 32) -and ($Interval -lt 1)) { + if ($Force) { + Write-Message -Message "Parameter FrequencyInterval must be provided for a recurring schedule. Setting it to first day of the week." -Level Warning + $Interval = 1 + } + else { + Stop-Function -Message "Parameter FrequencyInterval must be provided for a recurring schedule." -Target $SqlInstance + return + } + } + + # Setup the regex + $RegexDate = '(? + [CmdletBinding()] + Param ( + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(Mandatory, ValueFromPipeline)] + [DbaInstanceParameter[]]$ServerName, + [parameter(Mandatory)] + [string]$Alias, + [ValidateSet("TCPIP", "NamedPipes")] + [string]$Protocol = "TCPIP", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # This is a script block so cannot use messaging system + $scriptblock = { + $basekeys = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer", "HKLM:\SOFTWARE\Microsoft\MSSQLServer" + $ServerName = $args[0] + $Alias = $args[1] + $serverstring = $args[2] + + if ($env:PROCESSOR_ARCHITECTURE -like "*64*") { $64bit = $true } + + foreach ($basekey in $basekeys) { + if ($64bit -ne $true -and $basekey -like "*WOW64*") { continue } + + if ((Test-Path $basekey) -eq $false) { + throw "Base key ($basekey) does not exist. Quitting." + } + + $client = "$basekey\Client" + + if ((Test-Path $client) -eq $false) { + # "Creating $client key" + $null = New-Item -Path $client -Force + } + + $connect = "$client\ConnectTo" + + if ((Test-Path $connect) -eq $false) { + # "Creating $connect key" + $null = New-Item -Path $connect -Force + } + + if ($basekey -like "*WOW64*") { + $architecture = "32-bit" + } + else { + $architecture = "64-bit" + } + + # Write-Verbose "Creating/updating alias for $ComputerName for $architecture" + $null = New-ItemProperty -Path $connect -Name $Alias -Value $serverstring -PropertyType String -Force + } + } + } + + process { + if ($protocol -eq "TCPIP") { + $serverstring = "DBMSSOCN,$ServerName" + } + else { + $serverstring = "DBNMPNTW,\\$ServerName\pipe\sql\query" + } + + foreach ($computer in $ComputerName.ComputerName) { + + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + if ($PScmdlet.ShouldProcess($computer, "Adding $alias")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ErrorAction Stop -ArgumentList $ServerName, $Alias, $serverstring + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + + Get-DbaClientAlias -ComputerName $computer -Credential $Credential | Where-Object AliasName -eq $Alias + } +} +function New-DbaCmConnection { + <# + .SYNOPSIS + Generates a connection object for use in remote computer management. + + .DESCRIPTION + Generates a connection object for use in remote computer management. + Those objects are used for the purpose of cim/wmi queries, caching which protocol worked, optimizing performance and minimizing authentication errors. + + New-DbaCmConnection will create a NEW object and overwrite any existing ones for the specified computer. + Furthermore, information stored in the input beyond the computername will be discarded in favor of the new settings. + + Unless the connection cache has been disabled, all connections will automatically be registered in the cache, so no further action is necessary. + The output is primarily for information purposes, however it may be used to pass objects and circumvent the cache with those. + + NOTE: Generally, this function need not be used, as a first connection to a computer using any connecting function such as "Get-DbaCmObject" will automatically register a new default connection for it. + + This function exists to be able to preconfigure connections. + + .PARAMETER ComputerName + The computer to build the connection object for. + + .PARAMETER Credential + The credential to register. + + .PARAMETER UseWindowsCredentials + Whether using the default windows credentials is legit. + Not setting this will not exclude using windows credentials, but only not pre-confirm them as working. + + .PARAMETER OverrideExplicitCredential + Setting this will enable the credential override. + The override will cause the system to ignore explicitly specified credentials, so long as known, good credentials are available. + + .PARAMETER DisabledConnectionTypes + Exlicitly disable connection types. + These types will then not be used for connecting to the computer. + + .PARAMETER DisableBadCredentialCache + Will prevent the caching of credentials if set to true. + + .PARAMETER DisableCimPersistence + Will prevent Cim-Sessions to be reused. + + .PARAMETER DisableCredentialAutoRegister + Will prevent working credentials from being automatically cached + + .PARAMETER EnableCredentialFailover + Will enable automatic failing over to known to work credentials, when using bad credentials. + By default, passing bad credentials will cause the Computer Management functions to interrupt with a warning (Or exception if in silent mode). + + .PARAMETER WindowsCredentialsAreBad + Will prevent the windows credentials of the currently logged on user from being used for the remote connection. + + .PARAMETER CimWinRMOptions + Specify a set of options to use when connecting to the target computer using CIM over WinRM. + Use 'New-CimSessionOption' to create such an object. + + .PARAMETER CimDCOMOptions + Specify a set of options to use when connecting to the target computer using CIM over DCOM. + Use 'New-CimSessionOption' to create such an object. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaCmConnection + + .EXAMPLE + New-DbaCmConnection -ComputerName sql2014 -UseWindowsCredentials -OverrideExplicitCredential -DisabledConnectionTypes CimRM + + Returns a new configuration object for connecting to the computer sql2014. + - The current user credentials are set as valid + - The connection is configured to ignore explicit credentials (so all connections use the windows credentials) + - The connections will not try using CIM over WinRM + + Unless caching is globally disabled, this is automatically stored in the connection cache and will be applied automatically. + In that (the default) case, the output is for information purposes only and need not be used. + + .EXAMPLE + Get-Content computers.txt | New-DbaCmConnection -Credential $cred -CimWinRMOptions $options -DisableBadCredentialCache -OverrideExplicitCredential + + Gathers a list of computers from a text file, then creates and registers connections for each of them, setting them to ... + - use the credentials stored in $cred + - use the options stored in $options when connecting using CIM over WinRM + - not store credentials that are known to not work + - to ignore explicitly specified credentials + + Essentially, this configures all connections to those computers to prefer failure with the specified credentials over using alternative credentials. + #> + [CmdletBinding(DefaultParameterSetName = 'Credential')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName = $env:COMPUTERNAME, + [Parameter(ParameterSetName = "Credential")] + [PSCredential] + $Credential, + [Parameter(ParameterSetName = "Windows")] + [switch] + $UseWindowsCredentials, + [switch] + $OverrideExplicitCredential, + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType] + $DisabledConnectionTypes = 'None', + [switch] + $DisableBadCredentialCache, + [switch] + $DisableCimPersistence, + [switch] + $DisableCredentialAutoRegister, + [switch] + $EnableCredentialFailover, + [Parameter(ParameterSetName = "Credential")] + [switch] + $WindowsCredentialsAreBad, + [Microsoft.Management.Infrastructure.Options.WSManSessionOptions] + $CimWinRMOptions, + [Microsoft.Management.Infrastructure.Options.DComSessionOptions] + $CimDCOMOptions, + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + Write-Message -Level InternalComment -Message "Starting execution" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $disable_cache = Get-DbaConfigValue -Name 'ComputerManagement.Cache.Disable.All' -Fallback $false + } + process { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Processing computer: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection + + $connection = New-Object -TypeName Sqlcollaborative.Dbatools.Connection.ManagementConnection -ArgumentList $connectionObject.Connection.ComputerName + if (Test-Bound "Credential") { $connection.Credentials = $Credential } + if (Test-Bound "UseWindowsCredentials") { + $connection.Credentials = $null + $connection.UseWindowsCredentials = $UseWindowsCredentials + } + if (Test-Bound "OverrideExplicitCredential") { $connection.OverrideExplicitCredential = $OverrideExplicitCredential } + if (Test-Bound "DisabledConnectionTypes") { $connection.DisabledConnectionTypes = $DisabledConnectionTypes } + if (Test-Bound "DisableBadCredentialCache") { $connection.DisableBadCredentialCache = $DisableBadCredentialCache } + if (Test-Bound "DisableCimPersistence") { $connection.DisableCimPersistence = $DisableCimPersistence } + if (Test-Bound "DisableCredentialAutoRegister") { $connection.DisableCredentialAutoRegister = $DisableCredentialAutoRegister } + if (Test-Bound "EnableCredentialFailover") { $connection.DisableCredentialAutoRegister = $EnableCredentialFailover } + if (Test-Bound "WindowsCredentialsAreBad") { $connection.WindowsCredentialsAreBad = $WindowsCredentialsAreBad } + if (Test-Bound "CimWinRMOptions") { $connection.CimWinRMOptions = $CimWinRMOptions } + if (Test-Bound "CimDCOMOptions") { $connection.CimDCOMOptions = $CimDCOMOptions } + + if (-not $disable_cache) { + Write-Message -Level Verbose -Message "Writing connection to cache" + [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$connectionObject.Connection.ComputerName] = $connection + } + else { Write-Message -Level Verbose -Message "Skipping writing to cache, since the cache has been disabled!" } + $connection + } + } + end { + Write-Message -Level InternalComment -Message "Stopping execution" + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaComputerCertificate { + <# + .SYNOPSIS + Creates a new computer certificate useful for Forcing Encryption + + .DESCRIPTION + Creates a new computer certificate - self-signed or signed by an Active Directory CA, using the Web Server certificate. + + By default, a key with a length of 1024 and a friendly name of the machines FQDN is generated. + + This command was originally intended to help automate the process so that SSL certificates can be available for enforcing encryption on connections. + + It makes a lot of assumptions - namely, that your account is allowed to auto-enroll and that you have permission to do everything it needs to do ;) + + References: + http://sqlmag.com/sql-server/7-steps-ssl-encryption + https://azurebi.jppp.org/2016/01/23/using-lets-encrypt-certificates-for-secure-sql-server-connections/ + https://blogs.msdn.microsoft.com/sqlserverfaq/2016/09/26/creating-and-registering-ssl-certificates/ + + The certificate is generated using AD's webserver SSL template on the client machine and pushed to the remote machine. + + .PARAMETER ComputerName + The target SQL Server - defaults to localhost. If target is a cluster, you must also specify ClusterInstanceName (see below) + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. + + .PARAMETER CaServer + Optional - the CA Server where the request will be sent to + + .PARAMETER CaName + The properly formatted CA name of the corresponding CaServer + + .PARAMETER ClusterInstanceName + When creating certs for a cluster, use this parameter to create the certificate for the cluster node name. Use ComputerName for each of the nodes. + + .PARAMETER Password + Password to encrypt/decrypt private key for export to remote machine + + .PARAMETER FriendlyName + The FriendlyName listed in the certificate. This defaults to the FQDN of the $ComputerName + + .PARAMETER CertificateTemplate + The domain's Certificate Template - WebServer by default. + + .PARAMETER KeyLength + The length of the key - defaults to 1024 + + .PARAMETER Store + Certificate store - defaults to LocalMachine + + .PARAMETER Folder + Certificate folder - defaults to My (Personal) + + .PARAMETER Dns + Specify the Dns entries listed in SAN. By default, it will be ComputerName + FQDN, or in the case of clusters, clustername + cluster FQDN. + + .PARAMETER SelfSigned + Creates a self-signed certificate. All other parameters can still apply except CaServer and CaName because the command does not go and get the certificate signed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + New-DbaComputerCertificate + + Creates a computer certificate signed by the local domain CA for the local machine with the keylength of 1024. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName Server1 + + Creates a computer certificate signed by the local domain CA _on the local machine_ for server1 with the keylength of 1024. + + The certificate is then copied to the new machine over WinRM and imported. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName sqla, sqlb -ClusterInstanceName sqlcluster -KeyLength 4096 + + Creates a computer certificate for sqlcluster, signed by the local domain CA, with the keylength of 4096. + + The certificate is then copied to sqla _and_ sqlb over WinRM and imported. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName Server1 -WhatIf + + Shows what would happen if the command were run + + .EXAMPLE + New-DbaComputerCertificate -SelfSigned + + Creates a self-signed certificate + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [string]$CaServer, + [string]$CaName, + [string]$ClusterInstanceName, + [securestring]$Password, + [string]$FriendlyName = "SQL Server", + [string]$CertificateTemplate = "WebServer", + [int]$KeyLength = 1024, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [string[]]$Dns, + [switch]$SelfSigned, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $englishCodes = 9, 1033, 2057, 3081, 4105, 5129, 6153, 7177, 8201, 9225 + if ($englishCodes -notcontains (Get-DbaCmObject Win32_OperatingSystem).OSLanguage) { + Stop-Function -Message "Currently, this command is only supported in English OS locales. OS Locale detected: $([System.Globalization.CultureInfo]::GetCultureInfo([int](Get-DbaCmObject Win32_OperatingSystem).OSLanguage).DisplayName)`nWe apologize for the inconvenience and look into providing universal language support in future releases." + return + } + + if (-not (Test-ElevationRequirement -ComputerName $env:COMPUTERNAME)) { + return + } + + function GetHexLength { + [cmdletbinding()] + param( + [int]$strLen + ) + $hex = [String]::Format("{0:X2}", $strLen) + + if ($strLen -gt 127) { [String]::Format("{0:X2}", 128 + ($hex.Length / 2)) + $hex } + else { $hex } + } + + function Get-SanExt { + [cmdletbinding()] + param( + [string[]]$hostName + ) + # thanks to Lincoln of + # https://social.technet.microsoft.com/Forums/windows/en-US/f568edfa-7f93-46a4-aab9-a06151592dd9/converting-ascii-to-asn1-der + + $temp = '' + foreach ($fqdn in $hostName) { + # convert each character of fqdn to hex + $hexString = ($fqdn.ToCharArray() | ForEach-Object { [String]::Format("{0:X2}", [int]$_) }) -join '' + + # length of hex fqdn, in hex + $hexLength = GetHexLength ($hexString.Length / 2) + + # concatenate special code 82, hex length, hex string + $temp += "82${hexLength}${hexString}" + } + # calculate total length of concatenated string, in hex + $totalHexLength = GetHexLength ($temp.Length / 2) + # concatenate special code 30, hex length, hex string + $temp = "30${totalHexLength}${temp}" + # convert to binary + $bytes = $( + for ($i = 0; $i -lt $temp.Length; $i += 2) { + [byte]"0x$($temp.SubString($i, 2))" + } + ) + # convert to base 64 + $base64 = [Convert]::ToBase64String($bytes) + # output in proper format + for ($i = 0; $i -lt $base64.Length; $i += 64) { + $line = $base64.SubString($i, [Math]::Min(64, $base64.Length - $i)) + if ($i -eq 0) { "2.5.29.17=$line" } + else { "_continue_=$line" } + } + } + + if ((!$CaServer -or !$CaName) -and !$SelfSigned) { + try { + Write-Message -Level Verbose -Message "No CaServer or CaName specified. Performing lookup." + # hat tip Vadims Podans + $domain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name + $domain = "DC=" + $domain -replace '\.', ", DC=" + $pks = [ADSI]"LDAP://CN=Enrollment Services, CN=Public Key Services, CN=Services, CN=Configuration, $domain" + $cas = $pks.psBase.Children + + $allCas = @() + foreach ($ca in $cas) { + $allCas += [pscustomobject]@{ + CA = $ca | ForEach-Object { $_.Name } + Computer = $ca | ForEach-Object { $_.DNSHostName } + } + } + } + catch { + Stop-Function -Message "Cannot access Active Directory or find the Certificate Authority" -ErrorRecord $_ + return + } + + if (!$CaServer) { + $CaServer = ($allCas | Select-Object -First 1).Computer + Write-Message -Level Verbose -Message "Root Server: $CaServer" + } + + if (!$CaName) { + $CaName = ($allCas | Select-Object -First 1).CA + Write-Message -Level Verbose -Message "Root CA name: $CaName" + } + } + + $tempDir = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $certTemplate = "CertificateTemplate:$CertificateTemplate" + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($computer in $ComputerName) { + + if (!$secondaryNode) { + + if ($ClusterInstanceName) { + if ($ClusterInstanceName -notmatch "\.") { + $fqdn = "$ClusterInstanceName.$env:USERDNSDOMAIN" + } + else { + $fqdn = $ClusterInstanceName + } + } + else { + $resolved = Resolve-DbaNetworkName -ComputerName $computer.ComputerName -WarningAction SilentlyContinue + + if (!$resolved) { + $fqdn = "$ComputerName.$env:USERDNSDOMAIN" + Write-Message -Level Warning -Message "Server name cannot be resolved. Guessing it's $fqdn" + } + else { + $fqdn = $resolved.fqdn + } + } + + $certDir = "$tempDir\$fqdn" + $certCfg = "$certDir\request.inf" + $certCsr = "$certDir\$fqdn.csr" + $certCrt = "$certDir\$fqdn.crt" + $certPfx = "$certDir\$fqdn.pfx" + $tempPfx = "$certDir\temp-$fqdn.pfx" + + if (Test-Path($certDir)) { + Write-Message -Level Output -Message "Deleting files from $certDir" + $null = Remove-Item "$certDir\*.*" + } + else { + Write-Message -Level Output -Message "Creating $certDir" + $null = New-Item -Path $certDir -ItemType Directory -Force + } + + # Make sure output is compat with clusters + $shortName = $fqdn.Split(".")[0] + + if (!$dns) { + $dns = $shortName, $fqdn + } + + $san = Get-SanExt $dns + # Write config file + Set-Content $certCfg "[Version]" + Add-Content $certCfg 'Signature="$Windows NT$"' + Add-Content $certCfg "[NewRequest]" + Add-Content $certCfg "Subject = ""CN=$fqdn""" + Add-Content $certCfg "KeySpec = 1" + Add-Content $certCfg "KeyLength = $KeyLength" + Add-Content $certCfg "Exportable = TRUE" + Add-Content $certCfg "MachineKeySet = TRUE" + Add-Content $certCfg "FriendlyName=""$FriendlyName""" + Add-Content $certCfg "SMIME = False" + Add-Content $certCfg "PrivateKeyArchive = FALSE" + Add-Content $certCfg "UserProtected = FALSE" + Add-Content $certCfg "UseExistingKeySet = FALSE" + Add-Content $certCfg "ProviderName = ""Microsoft RSA SChannel Cryptographic Provider""" + Add-Content $certCfg "ProviderType = 12" + if ($SelfSigned) { + Add-Content $certCfg "RequestType = Cert" + } + else { + Add-Content $certCfg "RequestType = PKCS10" + } + Add-Content $certCfg "KeyUsage = 0xa0" + Add-Content $certCfg "[EnhancedKeyUsageExtension]" + Add-Content $certCfg "OID=1.3.6.1.5.5.7.3.1" + Add-Content $certCfg "[Extensions]" + Add-Content $certCfg $san + Add-Content $certCfg "Critical=2.5.29.17" + + + if ($PScmdlet.ShouldProcess("local", "Creating certificate for $computer")) { + Write-Message -Level Output -Message "Running: certreq -new $certCfg $certCsr" + $create = certreq -new $certCfg $certCsr + } + + if ($SelfSigned) { + $serial = (($create -Split "Serial Number:" -Split "Subject")[2]).Trim() # D: + $storedCert = Get-ChildItem Cert:\LocalMachine\My -Recurse | Where-Object SerialNumber -eq $serial + + if ($computer.IsLocalHost) { + $storedCert | Select-Object * | Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + } + else { + if ($PScmdlet.ShouldProcess("local", "Submitting certificate request for $computer to $CaServer\$CaName")) { + Write-Message -Level Output -Message "certreq -submit -config `"$CaServer\$CaName`" -attrib $certTemplate $certCsr $certCrt $certPfx" + $submit = certreq -submit -config ""$CaServer\$CaName"" -attrib $certTemplate $certCsr $certCrt $certPfx + } + + if ($submit -match "ssued") { + Write-Message -Level Output -Message "certreq -accept -machine $certCrt" + $null = certreq -accept -machine $certCrt + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($certCrt, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) + $storedCert = Get-ChildItem "Cert:\$store\$folder" -Recurse | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + } + elseif ($submit) { + Write-Message -Level Warning -Message "Something went wrong" + Write-Message -Level Warning -Message "$create" + Write-Message -Level Warning -Message "$submit" + Stop-Function -Message "Failure when attempting to create the cert on $computer. Exception: $_" -ErrorRecord $_ -Target $computer -Continue + } + + if ($Computer.IsLocalHost) { + $storedCert | Select-Object * | Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + } + } + + if (!$Computer.IsLocalHost) { + + if (!$secondaryNode) { + if ($PScmdlet.ShouldProcess("local", "Generating pfx and reading from disk")) { + Write-Message -Level Output -Message "Exporting PFX with password to $tempPfx" + $certdata = $storedCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::PFX, $password) + } + + if ($PScmdlet.ShouldProcess("local", "Removing cert from disk but keeping it in memory")) { + $storedCert | Remove-Item + } + + if ($ClusterInstanceName) { $secondaryNode = $true } + } + + $scriptblock = { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($args[0], $args[1], "Exportable,PersistKeySet") + + $certstore = New-Object System.Security.Cryptography.X509Certificates.X509Store($args[3], $args[2]) + $certstore.Open('ReadWrite') + $certstore.Add($cert) + $certstore.Close() + Get-ChildItem "Cert:\$($args[2])\$($args[3])" -Recurse | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $computer to import new cert")) { + try { + Write-Message -Level Output -Message "Connecting to $computer" + Invoke-Command2 -ComputerName $computer -Credential $Credential -ArgumentList $certdata, $Password, $Store, $Folder -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -Property DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + catch { + Stop-Function -Message "Issue importing new cert on $computer" -ErrorRecord $_ -Target $computer -Continue + } + } + } + if ($PScmdlet.ShouldProcess("local", "Removing all files from $certDir")) { + try { + Remove-Item -Force -Recurse $certDir -ErrorAction SilentlyContinue + } + catch { + Stop-Function "Isue removing files from $certDir" -Target $certDir -ErrorRecord $_ + } + } + } + } +} +function New-DbaCredential { + <# +.SYNOPSIS +Creates a new SQL Server credential + +.DESCRIPTION +Creates a new credential + +.PARAMETER SqlInstance +The target SQL Server(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Name +The Credential name + +.PARAMETER Identity +The Credential Identity + +.PARAMETER Password +Secure string used to authenticate the Credential Identity + +.PARAMETER MappedClassType +Sets the class associated with the credential. + +.PARAMETER ProviderName +Sets the name of the provider + +.PARAMETER Force +If credential exists, drop and recreate + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 + +You will be prompted to securely enter your password, then a credential will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a credential in the 'db1' database + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Name AzureBackupBlobStore -Identity '' -Password (ConvertTo-SecureString '' -AsPlainText -Force) + +Create credential on SQL Server 2012 CU2, SQL Server 2014 for use with BACKUP TO URL. +CredentialIdentity needs to be supplied with the Azure Storage Account Name. +Password needs to be one of the Access Keys for the account. + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Name 'https://.blob.core.windows.net/' -Identity 'SHARED ACCESS SIGNATURE' -Password (ConvertTo-SecureString '' -AsPlainText -Force) + +Create Credential on SQL Server 2016 or higher for use with BACKUP TO URL. +Name has to be the full URL for the blob store container that will be the backup target. +Password needs to be passed the Shared Access Token (SAS Key). + +#> + [CmdletBinding(SupportsShouldProcess = $true)] #, ConfirmImpact = "High" + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Name = $Identity, + [parameter(Mandatory)] + [Alias("CredentialIdentity")] + [string[]]$Identity, + [Security.SecureString]$Password, + [ValidateSet('CryptographicProvider', 'None')] + [string]$MappedClassType = "None", + [string]$ProviderName, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $mappedclass = switch ($MappedClassType) { + "CryptographicProvider" { 1 } + "None" { 0 } + } + } + + process { + if (!$Password) { + Read-Host -AsSecureString -Prompt "Enter the credential password" + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($cred in $Identity) { + $currentcred = $server.Credentials[$name] + + if ($currentcred) { + if ($force) { + Write-Message -Level Verbose -Message "Dropping credential $name" + $currentcred.Drop() + } + else { + Stop-Function -Message "Credential exists and Force was not specified" -Target $name -Continue + } + } + + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating credential for database '$cred' on $instance")) { + try { + $credential = New-Object Microsoft.SqlServer.Management.Smo.Credential -ArgumentList $server, $name + $credential.MappedClassType = $mappedclass + $credential.ProviderName = $ProviderName + $credential.Create($Identity, $Password) + + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $credential -Property ComputerName, InstanceName, SqlInstance, Name, Identity, CreateDate, MappedClassType, ProviderName + } + catch { + Stop-Function -Message "Failed to create credential in $cred on $instance. Exception: $($_.Exception.InnerException)" -Target $credential -InnerErrorRecord $_ -Continue + } + } + } + } + } +} +function New-DbaDatabaseMasterKey { + <# +.SYNOPSIS +Creates a new database master key + +.DESCRIPTION +Creates a new database master key. If no database is specified, the master key will be created in master. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the master key will be created. Defaults to master. + +.PARAMETER Password +Secure string used to create the key. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaDatabaseMasterKey -SqlInstance Server1 + +You will be prompted to securely enter your password, then a master key will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a master key in the 'db1' database + + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database = "master", + [parameter(Mandatory)] + [Security.SecureString]$Password = (Read-Host "Password" -AsSecureString), + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + $smodb = $server.Databases[$db] + + if ($null -eq $smodb) { + Stop-Function -Message "Database '$db' does not exist on $instance" -Target $smodb -Continue + } + + if ($null -ne $smodb.MasterKey) { + Stop-Function -Message "Master key already exists in the $db database on $instance" -Target $smodb -Continue + } + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating master key for database '$db' on $instance")) { + try { + $masterkey = New-Object Microsoft.SqlServer.Management.Smo.MasterKey $smodb + $masterkey.Create(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)))) + + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Database -value $smodb + + Select-DefaultView -InputObject $masterkey -Property ComputerName, InstanceName, SqlInstance, Database, CreateDate, DateLastModified, IsEncryptedByServer + } + catch { + Stop-Function -Message "Failed to create master key in $db on $instance. Exception: $($_.Exception.InnerException)" -Target $masterkey -InnerErrorRecord $_ -Continue + } + } + } + } + } +} +function New-DbaDbCertificate { + <# +.SYNOPSIS +Creates a new database certificate + +.DESCRIPTION +Creates a new database certificate. If no database is specified, the certificate will be created in master. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the certificate will be created. Defaults to master. + +.PARAMETER Name +Optional name to create the certificate. Defaults to database name. + +.PARAMETER Subject +Optional subject to create the certificate. + +.PARAMETER StartDate +Optional secure string used to create the certificate. + +.PARAMETER ExpirationDate +Optional secure string used to create the certificate. + +.PARAMETER ActiveForServiceBrokerDialog +Optional secure string used to create the certificate. + +.PARAMETER Password +Optional password - if no password is supplied, the password will be protected by the master key + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaDbCertificate -SqlInstance Server1 + +You will be prompted to securely enter your password, then a certificate will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaDbCertificate -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a certificate in the 'db1' database +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Name, + [object[]]$Database = "master", + [string[]]$Subject, + [datetime]$StartDate = (Get-Date), + [datetime]$ExpirationDate = $StartDate.AddYears(5), + [switch]$ActiveForServiceBrokerDialog, + [Security.SecureString]$Password, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias New-DbaDatabaseCertificate + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + + $currentdb = $server.Databases[$db] | Where-Object IsAccessible + + if ($null -eq $currentdb) { + Stop-Function -Message "Database '$db' does not exist on $instance" -Target $server -Continue + } + + if ($null -eq $name) { + Write-Message -Level Verbose -Message "Name is NULL, setting it to '$db'" + $name = $db + } + if ($null -eq $subject) { + Write-Message -Level Verbose -Message "Subject is NULL, setting it to '$db Database Certificate'" + $subject = "$db Database Certificate" + } + + foreach ($cert in $name) { + if ($null -ne $currentdb.Certificates[$cert]) { + Stop-Function -Message "Certificate '$cert' already exists in the $db database on $instance" -Target $currentdb -Continue + } + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating certificate for database '$db' on $instance")) { + try { + $smocert = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Certificate $currentdb, $cert + + $smocert.StartDate = $StartDate + $smocert.Subject = $Subject + $smocert.ExpirationDate = $ExpirationDate + $smocert.ActiveForServiceBrokerDialog = $ActiveForServiceBrokerDialog + + if ($password.Length -gt 0) { + $smocert.Create(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)))) + } + else { + $smocert.Create() + } + + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name Database -value $currentdb.Name + + Select-DefaultView -InputObject $smocert -Property ComputerName, InstanceName, SqlInstance, Database, Name, Subject, StartDate, ActiveForServiceBrokerDialog, ExpirationDate, Issuer, LastBackupDate, Owner, PrivateKeyEncryptionType, Serial + } + catch { + if ($_.Exception.InnerException) { + $exception = $_.Exception.InnerException.ToString() -Split "System.Data.SqlClient.SqlException: " + $exception = ($exception[1] -Split "at Microsoft.SqlServer.Management.Common.ConnectionManager")[0] + } + else { + $exception = $_.Exception + } + + Stop-Function -Message "Failed to create certificate in $db on $instance. Exception: $exception" -Target $smocert -InnerErrorRecord $_ -Continue + } + } + } + } + } + } +} +#ValidationTags#FlowControl# +function New-DbaDbSnapshot { + <# + .SYNOPSIS + Creates database snapshots + + .DESCRIPTION + Creates database snapshots without hassles + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER AllDatabases + Creates snapshot for all eligible databases + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER Name + The specific snapshot name you want to create. Works only if you target a single database. If you need to create multiple snapshot, + you must use the NameSuffix parameter + + .PARAMETER NameSuffix + When you pass a simple string, it'll be appended to use it to build the name of the snapshot. By default snapshots are created with yyyyMMdd_HHmmss suffix + You can also pass a standard placeholder, in which case it'll be interpolated (e.g. '{0}' gets replaced with the database name) + + .PARAMETER Path + Snapshot files will be created here (by default the filestructure will be created in the same folder as the base db) + + .PARAMETER Force + Databases with Filestream FG can be snapshotted, but the Filestream FG is marked offline + in the snapshot. To create a "partial" snapshot, you need to pass -Force explicitely + + NB: You can't then restore the Database from the newly-created snapshot. + For details, check https://msdn.microsoft.com/en-us/library/bb895334.aspx + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Restore, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaDbSnapshot + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Creates snapshot for HR and Accounting, returning a custom object displaying Server, Database, DatabaseCreated, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -Name HR_snap + + Creates snapshot named "HR_snap" for HR + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -NameSuffix 'fool_{0}_snap' + + Creates snapshot named "fool_HR_snap" for HR + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting -Path F:\snapshotpath + + Creates snapshots for HR and Accounting databases, storing files under the F:\snapshotpath\ dir + +#> + + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [string]$Name, + [string]$NameSuffix, + [string]$Path, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $NoSupportForSnap = @('model', 'master', 'tempdb') + # Evaluate the default suffix here for naming consistency + $DefaultSuffix = (Get-Date -Format "yyyyMMdd_HHmmss") + if ($NameSuffix.Length -gt 0) { + #Validate if Name can be interpolated + try { + $null = $NameSuffix -f 'some_string' + } + catch { + Stop-Function -Message "NameSuffix parameter must be a template only containing one parameter {0}" -ErrorRecord $_ + } + } + + function Resolve-SnapshotError($server) { + $errhelp = '' + $CurrentEdition = $server.Edition.toLower() + $CurrentVersion = $server.Version.Major * 1000000 + $server.Version.Minor * 10000 + $server.Version.Build + if ($server.Version.Major -lt 9) { + $errhelp = 'Not supported before 2005' + } + if ($CurrentVersion -lt 12002000 -and $errhelp.Length -eq 0) { + if ($CurrentEdition -notmatch '.*enterprise.*|.*developer.*|.*datacenter.*') { + $errhelp = 'Supported only for Enterprise, Developer or Datacenter editions' + } + } + $message = "" + if ($errhelp.Length -gt 0) { + $message += "Please make sure your version supports snapshots : ($errhelp)" + } + else { + $message += "This module can't tell you why the snapshot creation failed. Feel free to report back to dbatools what happened" + } + Write-Message -Level Warning -Message $message + } + } + process { + if (!$Database -and $AllDatabases -eq $false) { + Stop-Function -Message "You must specify a -AllDatabases or -Database to continue" -EnableException $EnableException + return + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + #Checks for path existence + if ($Path.Length -gt 0) { + if (!(Test-DbaSqlPath -SqlInstance $instance -Path $Path)) { + Stop-Function -Message "$instance cannot access the directory $Path" -ErrorRecord $_ -Target $instance -Continue -EnableException $EnableException + } + } + + if ($AllDatabases) { + $dbs = $server.Databases + } + + if ($Database) { + $dbs = $server.Databases | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $server.Databases | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + + $sourcedbs = @() + + ## double check for gotchas + foreach ($db in $dbs) { + if ($db.IsDatabaseSnapshot) { + Write-Message -Level Warning -Message "$($db.name) is a snapshot, skipping" + } + elseif ($db.name -in $NoSupportForSnap) { + Write-Message -Level Warning -Message "$($db.name) snapshots are prohibited" + } + elseif ($db.IsAccessible -ne $true) { + Write-Message -Level Verbose -Message "$($db.name) is not accessible, skipping" + } + else { + $sourcedbs += $db + } + } + if ($sourcedbs.Length -gt 1 -and $Name) { + Stop-Function -Message "You passed the Name parameter that is fixed but selected multiple databases to snapshot: use the NameSuffix parameter" -Continue -EnableException $EnableException + } + foreach ($db in $sourcedbs) { + if ($NameSuffix.Length -gt 0) { + $SnapName = $NameSuffix -f $db.Name + if ($SnapName -eq $NameSuffix) { + #no interpolation, just append + $SnapName = '{0}{1}' -f $db.Name, $NameSuffix + } + } + elseif ($Name.Length -gt 0) { + $SnapName = $Name + } + else { + $SnapName = "{0}_{1}" -f $db.Name, $DefaultSuffix + } + if ($SnapName -in $server.Databases.Name) { + Write-Message -Level Warning -Message "A database named $Snapname already exists, skipping" + continue + } + $all_FSD = $db.FileGroups | Where-Object FileGroupType -eq 'FileStreamDataFileGroup' + $all_MMO = $db.FileGroups | Where-Object FileGroupType -eq 'MemoryOptimizedDataFileGroup' + $has_FSD = $all_FSD.Count -gt 0 + $has_MMO = $all_MMO.Count -gt 0 + if ($has_MMO) { + Write-Message -Level Warning -Message "MEMORY_OPTIMIZED_DATA detected, snapshots are not possible" + continue + } + if ($has_FSD -and $Force -eq $false) { + Write-Message -Level Warning -Message "Filestream detected, skipping. You need to specify -Force. See Get-Help for details" + continue + } + $snaptype = "db snapshot" + if ($has_FSD) { + $snaptype = "partial db snapshot" + } + If ($Pscmdlet.ShouldProcess($instance, "Create $snaptype $SnapName of $($db.Name)")) { + $CustomFileStructure = @{ } + $counter = 0 + foreach ($fg in $db.FileGroups) { + $CustomFileStructure[$fg.Name] = @() + if ($fg.FileGroupType -eq 'FileStreamDataFileGroup') { + Continue + } + foreach ($file in $fg.Files) { + $counter += 1 + $basename = [IO.Path]::GetFileNameWithoutExtension($file.FileName) + $basepath = Split-Path $file.FileName -Parent + # change path if specified + if ($Path.Length -gt 0) { + $basepath = $Path + } + # we need to avoid cases where basename is the same for multiple FG + $fname = [IO.Path]::Combine($basepath, ("{0}_{1}_{2:0000}_{3:000}" -f $basename, $DefaultSuffix, (Get-Date).MilliSecond, $counter)) + # fixed extension is hardcoded as "ss", which seems a "de-facto" standard + $fname = [IO.Path]::ChangeExtension($fname, "ss") + $CustomFileStructure[$fg.Name] += @{ 'name' = $file.name; 'filename' = $fname } + } + } + $SnapDB = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Database -ArgumentList $server, $Snapname + $SnapDB.DatabaseSnapshotBaseName = $db.Name + foreach ($fg in $CustomFileStructure.Keys) { + $SnapFG = New-Object -TypeName Microsoft.SqlServer.Management.Smo.FileGroup $SnapDB, $fg + $SnapDB.FileGroups.Add($SnapFG) + foreach ($file in $CustomFileStructure[$fg]) { + $SnapFile = New-Object -TypeName Microsoft.SqlServer.Management.Smo.DataFile $SnapFG, $file['name'], $file['filename'] + $SnapDB.FileGroups[$fg].Files.Add($SnapFile) + } + } + + # we're ready to issue a Create, but SMO is a little uncooperative here + # there are cases we can manage and others we can't, and we need all the + # info we can get both from testers and from users + + $sql = $SnapDB.Script() + try { + $SnapDB.Create() + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $SnapDB.Name + SnapshotOf = $SnapDB.DatabaseSnapshotBaseName + SizeMB = [Math]::Round($SnapDB.Size, 2) ##FIXME, should use the stats for sparse files + DatabaseCreated = [dbadatetime]$SnapDB.createDate + PrimaryFilePath = $SnapDB.PrimaryFilePath + Status = 'Created' + Notes = $null + SnapshotDb = $SnapDB + } | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status + } + catch { + $Status = 'Created' + # here we manage issues we DO know about, hoping that the first command + # is always the one creating the snapshot, and give an helpful hint + try { + $server.Databases.Refresh() + if ($SnapName -notin $server.Databases.Name) { + # previous creation failed completely, snapshot is not there already + $null = $server.Query($sql[0]) + $server.Databases.Refresh() + $SnapDB = $server.Databases[$Snapname] + } + else { + $SnapDB = $server.Databases[$Snapname] + } + $Notes = @() + if ($db.ReadOnly -eq $true) { + $Notes += 'SMO is probably trying to set a property on a read-only snapshot, run with -Debug to find out and report back' + } + if ($has_FSD) { + $Status = 'Partial' + $Notes += 'Filestream groups are not viable for snapshot' + } + $Notes = $Notes -Join ';' + + $hints = @("Executing these commands led to a partial failure") + foreach ($stmt in $sql) { + $hints += $stmt + } + Write-Message -Level Debug -Message ($hints -Join "`n") + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $SnapDB.Name + SnapshotOf = $SnapDB.DatabaseSnapshotBaseName + SizeMB = [Math]::Round($SnapDB.Size, 2) ##FIXME, should use the stats for sparse files + DatabaseCreated = [dbadatetime]$SnapDB.createDate + PrimaryFilePath = $SnapDB.PrimaryFilePath + Status = $Status + Notes = $Notes + SnapshotDb = $SnapDB + } | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes + } + catch { + # we end up here when even the first issued command didn't create + # a valid snapshot + Write-Message -Level Warning -Message 'SMO failed to create the snapshot, run with -Debug to find out and report back' -ErrorRecord $_ + $hints = @("Executing these commands led to a failure") + foreach ($stmt in $sql) { + $hints += $stmt + } + Write-Message -Level Debug -Message ($hints -Join "`n") + Resolve-SnapshotError $server + } + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias New-DbaDatabaseSnapshot + } +} + +function New-DbaDbUser { + <# + .SYNOPSIS + Creates a new user for the specified database. + + .DESCRIPTION + Creates a new user for a specified database with provided specifications. + + .PARAMETER SqlInstance + The target SQL Server instance. Defaults to the default instance on localhost. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). To use: + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. + + To connect to SQL Server as a different Windows user, run PowerShell as that user. + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. By default, system databases are excluded. + + .PARAMETER IncludeSystem + If this switch is enabled, the user will be added to system databases. + + .PARAMETER Login + When specified, the user will be associated to this SQL login and have the same name as the Login. + + .PARAMETER Username + When specified, the user will have this name. + + .PARAMETER Force + If user exists, drop and recreate. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, User + Author: Frank Henninger (@osiris687) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaDbUser + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Login user1 + + Creates a new sql user with login named user1 in the specified database. + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Username user1 + + Creates a new sql user without login named user1 in the specified database. + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Login Login1 -Username user1 + + Creates a new sql user named user1 mapped to Login1 in the specified database. + + .EXAMPLE + Get-DbaDatabaseUser -SqlInstance sqlserver1 -Database DB1 | New-DbaDbUser -SqlInstance sqlserver2 -Database DB1 + + Copies users from sqlserver1.DB1 to sqlserver2.DB1. Does not copy permissions! + #> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "NoLogin")] + param( + [parameter(Mandatory, Position = 1)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystem, + [parameter(ParameterSetName = "Login")] + [string[]]$Login, + [parameter(ParameterSetName = "NoLogin")] + [parameter(ParameterSetName = "Login")] + [string[]]$Username, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Test-SqlLoginInDatabase { + param( + [Microsoft.SqlServer.Management.Smo.Login]$Login, + [Microsoft.SqlServer.Management.Smo.Database]$Database + ) + + # Does user exist with same login? + if ( $existingUser = ( $Database.Users | Where-Object Login -eq $smoLogin ) ) { + if (Test-Bound 'Force') { + if ($Pscmdlet.ShouldProcess($existingUser, "Dropping existing user $($existingUser.Name) because -Force was used")) { + try { + $existingUser.Drop() + } + catch { + Stop-Function -Message "Could not remove existing user $($existingUser.Name), skipping." -Target $existingUser -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + } + else { + Stop-Function -Message "User $($existingUser.Name) already exists and -Force was not specified" -Target $existingUser -Continue + } + } + } + + function Test-SqlUserInDatabase { + param( + [string[]]$Username, + [Microsoft.SqlServer.Management.Smo.Database]$Database + ) + + # Does user exist with same login? + if ( $existingUser = ( $Database.Users | Where-Object Name -eq $Username ) ) { + if (Test-Bound 'Force') { + if ($Pscmdlet.ShouldProcess($existingUser, "Dropping existing user $($existingUser.Name) because -Force was used")) { + try { + $existingUser.Drop() + } + catch { + Stop-Function -Message "Could not remove existing user $($existingUser.Name), skipping." -Target $existingUser -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + } + else { + Stop-Function -Message "User $($existingUser.Name) already exists and -Force was not specified" -Target $existingUser -Continue + } + } + } + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible -eq $true + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + if (Test-Bound 'IncludeSystem' -Not) { + $databases = $databases | Where-Object IsSystemObject -NE $true + } + + foreach ($db in $databases) { + Write-Message -Level Verbose -Message "Add users to Database $db on target $server" + + switch -Wildcard ($PSCmdlet.ParameterSetName) { + "Login*" { + # Creates a user with Login + Write-Message -Level VeryVerbose -Message "Using UserType: SqlLogin" + + if ($PSBoundParameters.Keys -notcontains 'Login') { + Stop-Function -Message "Parameter -Login is required " -Target $instance + } + if ($Login.GetType().Name -eq 'Login') { + $smoLogin = $Login + } + else { + #get the login associated with the given name. + $smoLogin = $server.Logins | Where-Object Name -eq $Login + if ($smoLogin -eq $null) { + Stop-Function -Message "Invalid Login: $Login is not found on $Server" -Target $instance; + return + } + } + + Test-SqlLoginInDatabase -Database $db -Login $smoLogin + + if ( $PSCmdlet.ParameterSetName -eq "LoginWithNewUsername" ) { + $Name = $Username + Write-Message -Level Verbose -Message "Using UserName: $Username" + } + else { + $Name = $smoLogin.Name + Write-Message -Level Verbose -Message "Using LoginName: $Name" + } + + $Login = $smoLogin + $UserType = [Microsoft.SqlServer.Management.Smo.UserType]::SqlLogin + } + + "NoLogin" { + # Creates a user without login + Write-Message -Level Verbose -Message "Using UserType: NoLogin" + $UserType = [Microsoft.SqlServer.Management.Smo.UserType]::NoLogin + $Name = $Username + } + } #switch + + # Does user exist with same name? + Test-SqlUserInDatabase -Database $db -Username $Name + + if ($Pscmdlet.ShouldProcess($db, "Creating user $Name")) { + try { + $smoUser = New-Object Microsoft.SqlServer.Management.Smo.User + $smoUser.Parent = $db + $smoUser.Name = $Name + + if ( $PSBoundParameters.Keys -contains 'Login' -and $Login.GetType().Name -eq 'Login' ) { + $smoUser.Login = Login + } + $smoUser.UserType = $UserType + + $smoUser.Create() + } + catch { + Stop-Function -Message "Failed to add user $Name in $db to $instance" -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + $smoUser.Refresh() + + if ( $PSBoundParameters.Keys -contains 'Username' -and $smoUser.Name -ne $Username ) { + $smoUser.Rename($Username) + } + + Write-Message -Level Verbose -Message "Successfully added $smoUser in $db to $instance." + } + + #Display Results + Get-DbaDatabaseUser -SqlInstance $server.Name -Database $db.Name | Where-Object name -eq $smoUser.Name + } #foreach ($db in $databases) + } #foreach ($instance in $SqlInstance) + } +} +function New-DbaLogin { + <# + .SYNOPSIS + Creates a new SQL Server login + + .DESCRIPTION + Creates a new SQL Server login with provided specifications + + .PARAMETER SqlInstance + The target SQL Server(s) + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials + + .PARAMETER Login + The Login name(s) + + .PARAMETER Password + Secure string used to authenticate the Login + + .PARAMETER HashedPassword + Hashed password string used to authenticate the Login + + .PARAMETER InputObject + Takes the parameters required from a Login object that has been piped into the command + + .PARAMETER LoginRenameHashtable + Pass a hash table into this parameter to change login names when piping objects into the procedure + + .PARAMETER MapToCertificate + Map the login to a certificate + + .PARAMETER MapToAsymmetricKey + Map the login to an asymmetric key + + .PARAMETER MapToCredential + Map the login to a credential + + .PARAMETER Sid + Provide an explicit Sid that should be used when creating the account. Can be [byte[]] or hex [string] ('0xFFFF...') + + .PARAMETER DefaultDatabase + Default database for the login + + .PARAMETER Language + Login's default language + + .PARAMETER PasswordExpiration + Enforces password expiration policy. Requires PasswordPolicy to be enabled. Can be $true or $false(default) + + .PARAMETER PasswordPolicy + Enforces password complexity policy. Can be $true or $false(default) + + .PARAMETER Disabled + Create the login in a disabled state + + .PARAMETER NewSid + Ignore sids from the piped login object to generate new sids on the server. Useful when copying login onto the same server + + .PARAMETER Force + If login exists, drop and recreate + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Login + Author: Kirill Kravtsov (@nvarscar) + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaLogin + + .EXAMPLE + New-DbaLogin -SqlInstance Server1,Server2 -Login Newlogin + + You will be prompted to securely enter the password for a login [Newlogin]. The login would be created on servers Server1 and Server2 with default parameters. + + .EXAMPLE + $securePassword = Read-Host "Input password" -AsSecureString + New-DbaLogin -SqlInstance Server1\sql1 -Login Newlogin -Password $securePassword -PasswordPolicy -PasswordExpiration + + Creates a login on Server1\sql1 with a predefined password. The login will have password and expiration policies enforced onto it. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql1 -Login Oldlogin | New-DbaLogin -SqlInstance sql1 -LoginRenameHashtable @{Oldlogin = 'Newlogin'} -Force -NewSid -Disabled:$false + + Copies a login [Oldlogin] to the same instance sql1 with the same parameters (including password). New login will have a new sid, a new name [Newlogin] and will not be disabled. Existing login [Newlogin] will be removed prior to creation. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql1 -Login Login1,Login2 | New-DbaLogin -SqlInstance sql2 -PasswordPolicy -PasswordExpiration -DefaultDatabase tempdb -Disabled + + Copies logins [Login1] and [Login2] from instance sql1 to instance sql2, but enforces password and expiration policies for the new logins. New logins will also have a default database set to [tempdb] and will be created in a disabled state. +#> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Password")] + param ( + [parameter(Mandatory, Position = 1)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Name", "LoginName")] + [parameter(ParameterSetName = "Password", Position = 2)] + [parameter(ParameterSetName = "PasswordHash")] + [parameter(ParameterSetName = "MapToCertificate")] + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [string[]]$Login, + [parameter(ValueFromPipeline = $true)] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [parameter(ParameterSetName = "MapToCertificate")] + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [object[]]$InputObject, + [Alias("Rename")] + [hashtable]$LoginRenameHashtable, + [parameter(ParameterSetName = "Password", Position = 3)] + [Security.SecureString]$Password, + [Alias("Hash", "PasswordHash")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$HashedPassword, + [parameter(ParameterSetName = "MapToCertificate")] + [string]$MapToCertificate, + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [string]$MapToAsymmetricKey, + [string]$MapToCredential, + [object]$Sid, + [Alias("DefaulDB")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$DefaultDatabase, + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$Language, + [Alias("Expiration", "CheckExpiration")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [switch]$PasswordExpiration, + [Alias("Policy", "CheckPolicy")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [switch]$PasswordPolicy, + [Alias("Disable")] + [switch]$Disabled, + [switch]$NewSid, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($Sid) { + if ($Sid.GetType().Name -ne 'Byte[]') { + foreach ($symbol in $Sid.TrimStart("0x").ToCharArray()) { + if ($symbol -notin "0123456789ABCDEF".ToCharArray()) { + Stop-Function -Message "Sid has invalid character '$symbol', cannot proceed." -Category InvalidArgument -EnableException $EnableException + return + } + } + $Sid = Convert-HexStringToByte $Sid + } + } + + if ($HashedPassword) { + if ($HashedPassword.GetType().Name -eq 'Byte[]') { + $HashedPassword = Convert-ByteToHexString $HashedPassword + } + } + } + + process { + #At least one of those should be specified + if (!($Login -or $InputObject)) { + Stop-Function -Message "No logins have been specified." -Category InvalidArgument -EnableException $EnableException + Return + } + + $loginCollection = @() + if ($InputObject) { + $loginCollection += $InputObject + if ($Login) { + Stop-Function -Message "Parameter -Login is not supported when processing objects from -InputObject. If you need to rename the logins, please use -LoginRenameHashtable." -Category InvalidArgument -EnableException $EnableException + Return + } + } + else { + $loginCollection += $Login + $Login | ForEach-Object { + if ($_.IndexOf('\') -eq -1 -and $PsCmdlet.ParameterSetName -like "Password*" -and !($Password -or $HashedPassword)) { + $passwordNotSpecified = $true + } + } + } + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($loginItem in $loginCollection) { + #check if $loginItem is an SMO Login object + if ($loginItem.GetType().Name -eq 'Login') { + #Get all the necessary fields + $loginName = $loginItem.Name + $loginType = $loginItem.LoginType + $currentSid = $loginItem.Sid + $currentDefaultDatabase = $loginItem.DefaultDatabase + $currentLanguage = $loginItem.Language + $currentPasswordExpiration = $loginItem.PasswordExpiration + $currentPasswordPolicyEnforced = $loginItem.PasswordPolicyEnforced + $currentDisabled = $loginItem.IsDisabled + + #Get previous password + if ($loginType -eq 'SqlLogin' -and !($Password -or $HashedPassword)) { + $sourceServer = $loginItem.Parent + switch ($sourceServer.versionMajor) { + 0 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM master.dbo.syslogins WHERE loginname='$loginName'" } + 8 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM dbo.syslogins WHERE name='$loginName'" } + 9 { $sql = "SELECT CONVERT(VARBINARY(256),password_hash) as hashedpass FROM sys.sql_logins where name='$loginName'" } + default { + $sql = "SELECT CAST(CONVERT(VARCHAR(256), CAST(LOGINPROPERTY(name,'PasswordHash') + AS VARBINARY(256)), 1) AS NVARCHAR(max)) AS hashedpass + FROM sys.server_principals + WHERE principal_id = $($loginItem.id)" + } + } + + try { + $hashedPass = $sourceServer.ConnectionContext.ExecuteScalar($sql) + } + catch { + $hashedPassDt = $sourceServer.Databases['master'].ExecuteWithResults($sql) + $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) + } + + if ($hashedPass.GetType().Name -ne "String") { + $hashedPass = Convert-ByteToHexString $hashedPass + } + $currentHashedPassword = $hashedPass + } + + #Get cryptography and attached credentials + if ($loginType -eq 'AsymmetricKey') { + $currentAsymmetricKey = $loginItem.AsymmetricKey + } + if ($loginType -eq 'Certificate') { + $currentCertificate = $loginItem.Certificate + } + #This method or property is accessible only while working with SQL Server 2008 or later. + if ($sourceServer.versionMajor -gt 9) { + if ($loginItem.EnumCredentials()) { + $currentCredential = $loginItem.EnumCredentials() + } + } + } + else { + $loginName = $loginItem + $currentSid = $currentDefaultDatabase = $currentLanguage = $currentPasswordExpiration = $currentAsymmetricKey = $currentCertificate = $currentCredential = $currentDisabled = $currentPasswordPolicyEnforced = $null + + if ($PsCmdlet.ParameterSetName -eq "MapToCertificate") { $loginType = 'Certificate' } + elseif ($PsCmdlet.ParameterSetName -eq "MapToAsymmetricKey") { $loginType = 'AsymmetricKey' } + elseif ($loginItem.IndexOf('\') -eq -1) { $loginType = 'SqlLogin' } + else { $loginType = 'WindowsUser' } + } + + if (($server.LoginMode -ne [Microsoft.SqlServer.Management.Smo.ServerLoginMode]::Mixed) -and ($loginType -eq 'SqlLogin')) { + Write-Message -Level Warning -Message "$instance does not have Mixed Mode enabled. [$loginName] is an SQL Login. Enable mixed mode authentication after the migration completes to use this type of login." -EnableException $EnableException + } + + if ($Sid) { + $currentSid = $Sid + } + if ($DefaultDatabase) { + $currentDefaultDatabase = $DefaultDatabase + } + if ($Language) { + $currentLanguage = $Language + } + if ($PSBoundParameters.Keys -contains 'PasswordExpiration') { + $currentPasswordExpiration = $PasswordExpiration + } + if ($PSBoundParameters.Keys -contains 'PasswordPolicy') { + $currentPasswordPolicyEnforced = $PasswordPolicy + } + if ($PSBoundParameters.Keys -contains 'MapToAsymmetricKey') { + $currentAsymmetricKey = $MapToAsymmetricKey + } + if ($PSBoundParameters.Keys -contains 'MapToCertificate') { + $currentCertificate = $MapToCertificate + } + if ($PSBoundParameters.Keys -contains 'MapToCredential') { + $currentCredential = $MapToCredential + } + if ($PSBoundParameters.Keys -contains 'Disabled') { + $currentDisabled = $Disabled + } + + #Apply renaming if necessary + if ($LoginRenameHashtable.Keys -contains $loginName) { + $loginName = $LoginRenameHashtable[$loginName] + } + + #Requesting password if required + if ($loginItem.GetType().Name -ne 'Login' -and $loginType -eq 'SqlLogin' -and !($Password -or $HashedPassword)) { + $Password = Read-Host -AsSecureString -Prompt "Enter a new password for the SQL Server login(s)" + } + + #verify if login exists on the server + if ($existingLogin = $server.Logins[$loginName]) { + if ($force) { + if ($Pscmdlet.ShouldProcess($existingLogin, "Dropping existing login $loginName on $instance because -Force was used")) { + try { + $existingLogin.Drop() + } + catch { + Stop-Function -Message "Could not remove existing login $loginName on $instance, skipping." -Target $loginName -Continue + } + } + } + else { + Stop-Function -Message "Login $loginName already exists on $instance and -Force was not specified" -Target $loginName -Continue + } + } + + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating login $loginName on $instance")) { + try { + $newLogin = New-Object Microsoft.SqlServer.Management.Smo.Login($server, $loginName) + $newLogin.LoginType = $loginType + + $withParams = "" + + if ($loginType -eq 'SqlLogin' -and $currentSid -and !$NewSid) { + Write-Message -Level Verbose -Message "Setting $loginName SID" + $withParams += ", SID = " + (Convert-ByteToHexString $currentSid) + $newLogin.Set_Sid($currentSid) + } + + if ($loginType -in ("WindowsUser", "WindowsGroup", "SqlLogin")) { + if ($currentDefaultDatabase) { + Write-Message -Level Verbose -Message "Setting $loginName default database to $currentDefaultDatabase" + $withParams += ", DEFAULT_DATABASE = [$currentDefaultDatabase]" + $newLogin.DefaultDatabase = $currentDefaultDatabase + } + + if ($currentLanguage) { + Write-Message -Level Verbose -Message "Setting $loginName language to $currentLanguage" + $withParams += ", DEFAULT_LANGUAGE = [$currentLanguage]" + $newLogin.Language = $currentLanguage + } + + #CHECK_EXPIRATION: default - OFF + if ($currentPasswordExpiration) { + $withParams += ", CHECK_EXPIRATION = ON" + $newLogin.PasswordExpirationEnabled = $true + } + else { + $withParams += ", CHECK_EXPIRATION = OFF" + $newLogin.PasswordExpirationEnabled = $false + } + + #CHECK_POLICY: default - ON + if ($currentPasswordPolicyEnforced) { + $withParams += ", CHECK_POLICY = ON" + $newLogin.PasswordPolicyEnforced = $true + } + else { + $withParams += ", CHECK_POLICY = OFF" + $newLogin.PasswordPolicyEnforced = $false + } + + #Generate hashed password if necessary + if ($Password) { + $currentHashedPassword = Get-PasswordHash $Password $server.versionMajor + } + elseif ($HashedPassword) { + $currentHashedPassword = $HashedPassword + } + } + elseif ($loginType -eq 'AsymmetricKey') { + $newLogin.AsymmetricKey = $currentAsymmetricKey + } + elseif ($loginType -eq 'Certificate') { + $newLogin.Certificate = $currentCertificate + } + + #Add credential + if ($currentCredential) { + $withParams += ", CREDENTIAL = [$currentCredential]" + } + + Write-Message -Level Verbose -Message "Adding as login type $loginType" + + # Attempt to add login using SMO, then T-SQL + try { + if ($loginType -in ("WindowsUser", "WindowsGroup", "AsymmetricKey", "Certificate")) { + if ($withParams) { $withParams = " WITH " + $withParams.TrimStart(',') } + $newLogin.Create() + } + elseif ($loginType -eq "SqlLogin") { + $newLogin.Create($currentHashedPassword, [Microsoft.SqlServer.Management.Smo.LoginCreateOptions]::IsHashed) + } + $newLogin.Refresh() + + #Adding credential + if ($currentCredential) { + try { + $newLogin.AddCredential($currentCredential) + } + catch { + $newLogin.Drop() + Stop-Function -Message "Failed to add $loginName to $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + Write-Message -Level Verbose -Message "Successfully added $loginName to $instance." + } + catch { + Write-Message -Level Verbose -Message "Failed to create $loginName on $instance using SMO, trying T-SQL." + try { + if ($loginType -eq 'AsymmetricKey') { $sql = "CREATE LOGIN [$loginName] FROM ASYMMETRIC KEY [$currentAsymmetricKey]" } + elseif ($loginType -eq 'Certificate') { $sql = "CREATE LOGIN [$loginName] FROM CERTIFICATE [$currentCertificate]" } + elseif ($loginType -eq "SqlLogin") { $sql = "CREATE LOGIN [$loginName] WITH PASSWORD = $currentHashedPassword HASHED" + $withParams } + else { $sql = "CREATE LOGIN [$loginName] FROM WINDOWS" + $withParams } + + $null = $server.Query($sql) + $newLogin = $server.logins[$loginName] + Write-Message -Level Verbose -Message "Successfully added $loginName to $instance." + } + catch { + Stop-Function -Message "Failed to add $loginName to $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + + #Process the Disabled property + if ($currentDisabled) { + try { + $newLogin.Disable() + Write-Message -Level Verbose -Message "Login $loginName has been disabled on $instance." + } + catch { + Write-Message -Level Verbose -Message "Failed to disable $loginName on $instance using SMO, trying T-SQL." + try { + $sql = "ALTER LOGIN [$loginName] DISABLE" + $null = $server.Query($sql) + Write-Message -Level Verbose -Message "Login $loginName has been disabled on $instance." + } + catch { + Stop-Function -Message "Failed to disable $loginName on $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + } + #Display results + Get-DbaLogin -SqlInstance $server -Login $loginName + } + catch { + Stop-Function -Message "Failed to create login $loginName on $instance." -Target $credential -InnerErrorRecord $_ -Continue + } + } + } + } + } +} +function New-DbaPublishProfile { + <# + .SYNOPSIS + Creates a new Publish Profile. + + .DESCRIPTION + The New-PublishProfile command generates a standard publish profile xml file that can be used by the DacFx (this and everything else) to control the deployment of your dacpac + This generates a standard template XML which is enough to dpeloy a dacpac but it is highly recommended that you add additional options to the publish profile. + If you use Visual Studio you can open a publish.xml file and use the ui to edit the file - + To create a new file, right click on an SSDT project, choose "Publish" then "Load Profile" and load your profile or create a new one. + Once you have loaded it in Visual Studio, clicking advanced shows you the list of options available to you. + For a full list of options that you can add to the profile, google "sqlpackage.exe command line switches" or (https://msdn.microsoft.com/en-us/library/hh550080(v=vs.103).aspx) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to and publish to. Alternatively, you can provide a ConnectionString. + + .PARAMETER SqlCredential + Allows you to login to servers using alternative logins instead Integrated, accepts Credential object created by Get-Credential + + .PARAMETER Database + The database name you are targeting + + .PARAMETER ConnectionString + The connection string to the database you are upgrading. + + Alternatively, you can provide a SqlInstance (and optionally SqlCredential) and the script will connect and generate the connectionstring. + + .PARAMETER Path + The directory where you would like to save the profile xml file(s). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Dacpac + Author: Richie lee (@bzzzt_io) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + .LINK + https://dbatools.io/New-DbaPublishProfile + + .EXAMPLE + New-DbaPublishProfile -SqlInstance sql2017 -SqlCredential (Get-Credential) -Database WorldWideImporters -Path C:\temp + + In this example, a prompt will appear for alternative credentials, tghen a connection will be made to sql2017. Using that connection, + the ConnectionString will be extracted and used within the Publish Profile XML file which will be created at C:\temp\sql2017-WorldWideImporters-publish.xml + + .EXAMPLE + New-DbaPublishProfile -Database WorldWideImporters -Path C:\temp -ConnectionString "SERVER=(localdb)\MSSQLLocalDB;Integrated Security=True;Database=master" + + In this example, no connections are made, and a Publish Profile XML would be created at C:\temp\localdb-MSSQLLocalDB-WorldWideImporters-publish.xml + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Parameter(Mandatory)] + [string[]]$Database, + [string]$Path = "$home\Documents", + [string[]]$ConnectionString, + [switch]$EnableException + ) + begin { + if ((Test-Bound -Not -ParameterName SqlInstance) -and (Test-Bound -Not -ParameterName ConnectionString)) { + Stop-Function -Message "You must specify either SqlInstance or ConnectionString" + } + + if (-not (Test-Path $Path)) { + Stop-Function -Message "$Path doesn't exist or access denied" + } + + if ((Get-Item $path) -isnot [System.IO.DirectoryInfo]) { + Stop-Function -Message "Path must be a directory" + } + + function Get-Template ($db, $connstring) { + " + + + {0} + {1} + 1 + + " -f $db[0], $connstring + } + + function Get-ServerName ($connstring) { + $builder = New-Object System.Data.Common.DbConnectionStringBuilder + $builder.set_ConnectionString($connstring) + $instance = $builder['data source'] + + if (-not $instance) { + $instance = $builder['server'] + } + + return $instance.ToString().Replace('\', '--') + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $ConnectionString += $server.ConnectionContext.ConnectionString.Replace(';Application Name="dbatools PowerShell module - dbatools.io"', '') + + } + + foreach ($connstring in $ConnectionString) { + foreach ($db in $Database) { + $profileTemplate = Get-Template $db, $connstring + $instancename = Get-ServerName $connstring + + try { + $server = [DbaInstance]($instancename.ToString().Replace('--', '\')) + $PublishProfile = Join-Path $Path "$($instancename.Replace('--','-'))-$db-publish.xml" -ErrorAction Stop + Write-Message -Level Verbose -Message "Writing to $PublishProfile" + $profileTemplate | Out-File $PublishProfile -ErrorAction Stop + [pscustomobject]@{ + ComputerName = $server.ComputerName + InstanceName = $server.InstanceName + SqlInstance = $server.FullName + Database = $db + FileName = $PublishProfile + ConnectionString = $connstring + ProfileTemplate = $profileTemplate + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, ProfileTemplate + } + catch { + Stop-Function -ErrorRecord $_ -Message "Failure" -Target $instancename -Continue + } + } + } + } +} +function New-DbaScriptingOption { + <# + .SYNOPSIS + Creates a new Microsoft.SqlServer.Management.Smo.ScriptingOptions object + + .DESCRIPTION + Creates a new Microsoft.SqlServer.Management.Smo.ScriptingOptions object. Basically saves you the time from remembering the SMO assembly name ;) + + See https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.scriptingoptions.aspx for more information + + .NOTES + Tags: Migration, Backup, DR + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaScriptingOption + https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.scriptingoptions.aspx + + .EXAMPLE + $options = New-DbaScriptingOption + $options.ScriptDrops = $false + $options.WithDependencies = $true + Get-DbaAgentJob -SqlInstance sql2016 | Export-DbaScript -ScriptingOptionObject $options + + Exports Agent Jobs with the Scripting Options ScriptDrops set to $false and WithDependencies set to true + + #> + New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions +} +function New-DbaServiceMasterKey { + <# +.SYNOPSIS +Creates a new service master key + +.DESCRIPTION +Creates a new service master key in the master database + +.PARAMETER SqlInstance +The target SQL Server instances + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Password +Secure string used to create the key. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaServiceMasterKey -SqlInstance Server1 + +You will be prompted to securely enter your Service Key Password twice, then a master key will be created in the master database on server1 if it does not exist. + +#> + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Security.SecureString]$Password, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + if (Test-Bound -ParameterName Password -Not) { + $password = Read-Host -AsSecureString -Prompt "You must enter Service Key password for $instance" + $password2 = Read-Host -AsSecureString -Prompt "Type the password again" + + if (([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) -ne ([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password2)))) { + Stop-Function -Message "Passwords do not match" -Continue + } + } + New-DbaDatabaseMasterKey -SqlInstance $instance -Database master -Password $password + } + } +} +function New-DbaSqlConnectionString { + <# + .SYNOPSIS + Builds or extracts a SQL Server Connection String + + .DESCRIPTION + Builds or extracts a SQL Server Connection String + + See https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.connectionstring.aspx + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnectionstringbuilder.aspx + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.aspx + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user be it Windows or SQL Server. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER AccessToken + Gets or sets the access token for the connection. + + .PARAMETER AppendConnectionString + Appends to the current connection string. Note that you cannot pass authentication information using this method. Use -SqlInstance and, optionally, -SqlCredential to set authentication information. + + .PARAMETER ApplicationIntent + Declares the application workload type when connecting to a server. Possible values are ReadOnly and ReadWrite. + + .PARAMETER BatchSeparator + By default, this is "GO" + + .PARAMETER ClientName + By default, this command sets the client to "dbatools PowerShell module - dbatools.io - custom connection" if you're doing anything that requires profiling, you can look for this client name. Using -ClientName allows you to set your own custom client. + + .PARAMETER ConnectTimeout + The length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error. + + Valid values are greater than or equal to 0 and less than or equal to 2147483647. + + When opening a connection to a Azure SQL Database, set the connection timeout to 30 seconds. + + .PARAMETER EncryptConnection + When true, SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed. Recognized values are true, false, yes, and no. For more information, see Connection String Syntax. + + Beginning in .NET Framework 4.5, when TrustServerCertificate is false and Encrypt is true, the server name (or IP address) in a SQL Server SSL certificate must exactly match the server name (or IP address) specified in the connection string. Otherwise, the connection attempt will fail. For information about support for certificates whose subject starts with a wildcard character (*), see Accepted wildcards used by server certificates for server authentication. + + .PARAMETER FailoverPartner + The name of the failover partner server where database mirroring is configured. + + If the value of this key is "", then Initial Catalog must be present, and its value must not be "". + + The server name can be 128 characters or less. + + If you specify a failover partner but the failover partner server is not configured for database mirroring and the primary server (specified with the Server keyword) is not available, then the connection will fail. + + If you specify a failover partner and the primary server is not configured for database mirroring, the connection to the primary server (specified with the Server keyword) will succeed if the primary server is available. + + .PARAMETER IsActiveDirectoryUniversalAuth + Azure related + + .PARAMETER LockTimeout + Sets the time in seconds required for the connection to time out when the current transaction is locked. + + .PARAMETER MaxPoolSize + Sets the maximum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MinPoolSize + Sets the minimum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MultipleActiveResultSets + When used, an application can maintain multiple active result sets (MARS). When false, an application must process or cancel all result sets from one batch before it can execute any other batch on that connection. + + .PARAMETER MultiSubnetFailover + If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting MultiSubnetFailover provides faster detection of and connection to the (currently) active server. For more information about SqlClient support for Always On Availability Groups + + .PARAMETER NetworkProtocol + Connect explicitly using 'TcpIp','NamedPipes','Multiprotocol','AppleTalk','BanyanVines','Via','SharedMemory' and 'NWLinkIpxSpx' + + .PARAMETER NonPooledConnection + Request a non-pooled connection + + .PARAMETER PacketSize + Sets the size in bytes of the network packets used to communicate with an instance of SQL Server. Must match at server. + + .PARAMETER PooledConnectionLifetime + When a connection is returned to the pool, its creation time is compared with the current time, and the connection is destroyed if that time span (in seconds) exceeds the value specified by Connection Lifetime. This is useful in clustered configurations to force load balancing between a running server and a server just brought online. + + A value of zero (0) causes pooled connections to have the maximum connection timeout. + + .PARAMETER SqlExecutionModes + The SqlExecutionModes enumeration contains values that are used to specify whether the commands sent to the referenced connection to the server are executed immediately or saved in a buffer. + + Valid values include CaptureSql, ExecuteAndCaptureSql and ExecuteSql. + + .PARAMETER StatementTimeout + Sets the number of seconds a statement is given to run before failing with a time-out error. + + .PARAMETER TrustServerCertificate + Sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust. + + .PARAMETER WorkstationId + Sets the name of the workstation connecting to SQL Server. + + .NOTES + Tags: Connection, Connect, ConnectionString + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSqlConnectionString + + .EXAMPLE + New-DbaSqlConnectionString -SqlInstance sql2014 + + Creates a connection string that connects using Windows Authentication + + .EXAMPLE + Connect-DbaInstance -SqlInstance sql2016 | New-DbaSqlConnectionString + + Builds a connected SMO object using Connect-DbaInstance then extracts and displays the connection string + + .EXAMPLE + $wincred = Get-Credential ad\sqladmin + New-DbaSqlConnectionString -SqlInstance sql2014 -Credential $wincred + + Creates a connection string that connects using alternative Windows credentials + + .EXAMPLE + $sqlcred = Get-Credential sqladmin + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -Credential $sqlcred + + Login to sql2014 as SQL login sqladmin. + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -ClientName "mah connection" + + Creates a connection string that connects using Windows Authentication and uses the client name "mah connection". So when you open up profiler or use extended events, you can search for "mah connection". + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -AppendConnectionString "Packet Size=4096;AttachDbFilename=C:\MyFolder\MyDataFile.mdf;User Instance=true;" + + Creates a connection string that connects to sql2014 using Windows Authentication, then it sets the packet size (this can also be done via -PacketSize) and other connection attributes. + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -NetworkProtocol TcpIp -MultiSubnetFailover + + Creates a connection string with Windows Authentication that uses TCPIP and has MultiSubnetFailover enabled. + + .EXAMPLE + $connstring = New-DbaSqlConnectionString sql2016 -ApplicationIntent ReadOnly + + Creates a connection string with ReadOnly ApplicantionIntent. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("SqlCredential")] + [PSCredential]$Credential, + [string]$AccessToken, + [ValidateSet('ReadOnly', 'ReadWrite')] + [string]$ApplicationIntent, + [string]$BatchSeparator, + [string]$ClientName = "custom connection", + [int]$ConnectTimeout, + [switch]$EncryptConnection, + [string]$FailoverPartner, + [switch]$IsActiveDirectoryUniversalAuth, + [int]$LockTimeout, + [int]$MaxPoolSize, + [int]$MinPoolSize, + [switch]$MultipleActiveResultSets, + [switch]$MultiSubnetFailover, + [ValidateSet('TcpIp', 'NamedPipes', 'Multiprotocol', 'AppleTalk', 'BanyanVines', 'Via', 'SharedMemory', 'NWLinkIpxSpx')] + [string]$NetworkProtocol, + [switch]$NonPooledConnection, + [int]$PacketSize, + [int]$PooledConnectionLifetime, + [ValidateSet('CaptureSql', 'ExecuteAndCaptureSql', 'ExecuteSql')] + [string]$SqlExecutionModes, + [int]$StatementTimeout, + [switch]$TrustServerCertificate, + [string]$WorkstationId, + [string]$AppendConnectionString + ) + + process { + foreach ($instance in $sqlinstance) { + + if ($instance.GetType() -eq [Microsoft.SqlServer.Management.Smo.Server]) { + return $instance.ConnectionContext.ConnectionString + } + else { + $guid = [System.Guid]::NewGuid() + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $guid + + if ($AppendConnectionString) { + $connstring = $server.ConnectionContext.ConnectionString + $server.ConnectionContext.ConnectionString = "$connstring;$appendconnectionstring" + $server.ConnectionContext.ConnectionString + } + else { + + $server.ConnectionContext.ApplicationName = $clientname + + if ($AccessToken) { $server.ConnectionContext.AccessToken = $AccessToken } + if ($BatchSeparator) { $server.ConnectionContext.BatchSeparator = $BatchSeparator } + if ($ConnectTimeout) { $server.ConnectionContext.ConnectTimeout = $ConnectTimeout } + if ($Database) { $server.ConnectionContext.DatabaseName = $Database } + if ($EncryptConnection) { $server.ConnectionContext.EncryptConnection = $true } + if ($IsActiveDirectoryUniversalAuth) { $server.ConnectionContext.IsActiveDirectoryUniversalAuth = $true } + if ($LockTimeout) { $server.ConnectionContext.LockTimeout = $LockTimeout } + if ($MaxPoolSize) { $server.ConnectionContext.MaxPoolSize = $MaxPoolSize } + if ($MinPoolSize) { $server.ConnectionContext.MinPoolSize = $MinPoolSize } + if ($MultipleActiveResultSets) { $server.ConnectionContext.MultipleActiveResultSets = $true } + if ($NetworkProtocol) { $server.ConnectionContext.NetworkProtocol = $NetworkProtocol } + if ($NonPooledConnection) { $server.ConnectionContext.NonPooledConnection = $true } + if ($PacketSize) { $server.ConnectionContext.PacketSize = $PacketSize } + if ($PooledConnectionLifetime) { $server.ConnectionContext.PooledConnectionLifetime = $PooledConnectionLifetime } + if ($StatementTimeout) { $server.ConnectionContext.StatementTimeout = $StatementTimeout } + if ($SqlExecutionModes) { $server.ConnectionContext.SqlExecutionModes = $SqlExecutionModes } + if ($TrustServerCertificate) { $server.ConnectionContext.TrustServerCertificate = $true } + if ($WorkstationId) { $server.ConnectionContext.WorkstationId = $WorkstationId } + + $connstring = $server.ConnectionContext.ConnectionString + if ($MultiSubnetFailover) { $connstring = "$connstring;MultiSubnetFailover=True" } + if ($FailoverPartner) { $connstring = "$connstring;Failover Partner=$FailoverPartner" } + if ($ApplicationIntent) { $connstring = "$connstring;ApplicationIntent=$ApplicationIntent;" } + + if ($connstring -ne $server.ConnectionContext.ConnectionString) { + $server.ConnectionContext.ConnectionString = $connstring + } + if ($null -ne $Credential.username) { + $username = ($Credential.username).TrimStart("\") + + if ($username -like "*\*") { + $username = $username.Split("\")[1] + $authtype = "Windows Authentication with Credential" + $server.ConnectionContext.LoginSecure = $true + $server.ConnectionContext.ConnectAsUser = $true + $server.ConnectionContext.ConnectAsUserName = $username + $server.ConnectionContext.ConnectAsUserPassword = ($Credential).GetNetworkCredential().Password + } + else { + $authtype = "SQL Authentication" + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($username) + $server.ConnectionContext.set_SecurePassword($Credential.Password) + } + } + + ($server.ConnectionContext.ConnectionString).Replace($guid, $SqlInstance) + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaSqlConnectionStringBuilder { + <# + .SYNOPSIS + Returns a System.Data.SqlClient.SqlConnectionStringBuilder with the string specified + + .DESCRIPTION + Creates a System.Data.SqlClient.SqlConnectionStringBuilder from a connection string. + + .PARAMETER ConnectionString + A Connection String + + .PARAMETER ApplicationName + The application name to tell SQL Server the connection is associated with. + + .PARAMETER DataSource + The Sql Server to connect to. + + .PARAMETER InitialCatalog + The initial database on the server to connect to. + + .PARAMETER IntegratedSecurity + Set to true to use windows authentication. + + .PARAMETER UserName + Sql User Name to connect with. + + .PARAMETER Password + Password to use to connect with. + + .PARAMETER MultipleActiveResultSets + Enable Multiple Active Result Sets. + + .PARAMETER ColumnEncryptionSetting + Enable Always Encrypted. + + .PARAMETER WorkstationID + Set the Workstation Id that is associated with the connection. + + .NOTES + Author: zippy1981 + Tags: SqlBuild, ConnectionString, Connection + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2017 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSqlConnectionStringBuilder + + .EXAMPLE + New-DbaSqlConnectionStringBuilder + + Returns an empty ConnectionStringBuilder + + .EXAMPLE + "Data Source=localhost,1433;Initial Catalog=AlwaysEncryptedSample;UID=sa;PWD=alwaysB3Encrypt1ng;Application Name=Always Encrypted Sample MVC App;Column Encryption Setting=enabled" | New-DbaSqlConnectionStringBuilder + + Returns a connection string builder that can be used to connect to the local sql server instance on the default port. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "")] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [string[]]$ConnectionString = "", + [Parameter(Mandatory = $false)] + [string]$ApplicationName = "dbatools Powershell Module", + [Parameter(Mandatory = $false)] + [string]$DataSource = $null, + [Parameter(Mandatory = $false)] + [string]$InitialCatalog = $null, + [Parameter(Mandatory = $false)] + [Nullable[bool]]$IntegratedSecurity = $null, + [Parameter(Mandatory = $false)] + [string]$UserName = $null, + # No point in securestring here, the memory is never stored securely in memory. + [Parameter(Mandatory = $false)] + [string]$Password = $null, + [Alias('MARS')] + [Parameter(Mandatory = $false)] + [switch]$MultipleActiveResultSets, + [Alias('AlwaysEncrypted')] + [Parameter(Mandatory = $false)] + [Data.SqlClient.SqlConnectionColumnEncryptionSetting]$ColumnEncryptionSetting = + [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled, + [Parameter(Mandatory = $false)] + [string]$WorkstationId = $env:COMPUTERNAME + ) + process { + foreach ($cs in $ConnectionString) { + $builder = New-Object Data.SqlClient.SqlConnectionStringBuilder $cs + if ($builder.ApplicationName -eq ".Net SqlClient Data Provider") { + $builder['Application Name'] = $ApplicationName + } + if (![string]::IsNullOrWhiteSpace($DataSource)) { + $builder['Data Source'] = $DataSource + } + if (![string]::IsNullOrWhiteSpace($InitialCatalog)) { + $builder['Initial Catalog'] = $InitialCatalog + } + if (![string]::IsNullOrWhiteSpace($IntegratedSecurity)) { + $builder['Integrated Security'] = $IntegratedSecurity + } + if (![string]::IsNullOrWhiteSpace($UserName)) { + $builder["User ID"] = $UserName + } + if (![string]::IsNullOrWhiteSpace($Password)) { + $builder['Password'] = $Password + } + if (![string]::IsNullOrWhiteSpace($WorkstationId)) { + $builder['Workstation ID'] = $WorkstationId + } + if ($MultipleActiveResultSets -eq $true) { + $builder['MultipleActiveResultSets'] = $true + } + if ($ColumnEncryptionSetting -eq [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled) { + $builder['Column Encryption Setting'] = [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled + } + $builder + } + } +} +function New-DbaSqlDirectory { + <# + .SYNOPSIS + Creates new path as specified by the path variable + + .DESCRIPTION + Uses master.dbo.xp_create_subdir to create the path + Returns $true if the path can be created, $false otherwise + + .PARAMETER SqlInstance + The SQL Server you want to run the test on. + + .PARAMETER Path + The Path to tests. Can be a file or directory. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .NOTES + Tags: Path, Directory, Folder + Author: Chrissy LeMaire (@cl), netnerds.net + + Requires: Admin access to server (not SQL Services), + Remoting must be enabled and accessible if $SqlInstance is not local + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSqlDirectory + + .EXAMPLE + New-DbaSqlDirectory -SqlInstance sqlcluster -Path L:\MSAS12.MSSQLSERVER\OLAP + + If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. + + .EXAMPLE + $credential = Get-Credential + New-DbaSqlDirectory -SqlInstance sqlcluster -SqlCredential $credential -Path L:\MSAS12.MSSQLSERVER\OLAP + + If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. Uses a SqlCredential to connect + #> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Parameter(Mandatory = $true)] + [string]$Path, + [PSCredential]$SqlCredential + ) + + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + + $Path = $Path.Replace("'", "''") + + $exists = Test-DbaSqlPath -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Path $Path + + if ($exists) { + Write-Warning "$Path already exists" + return + } + + $sql = "EXEC master.dbo.xp_create_subdir'$path'" + Write-Debug $sql + + try { + $query = $server.Query($sql) + $Created = $true + } + catch { + $Created = $false + } + + [pscustomobject]@{ + Server = $SqlInstance + Path = $Path + Created = $Created + } +} +function New-DbaSsisCatalog { + <# + .SYNOPSIS + Enables the SSIS Catalog on a SQL Server 2012+ + + .DESCRIPTION + After installing the SQL Server Engine and SSIS you still have to enable the SSIS Catalog. This function will enable the catalog and gives the option of supplying the password. + + .PARAMETER SqlInstance + SQL Server you wish to run the function on. + + .PARAMETER SqlCredential + Credentials used to connect to the SQL Server + + .PARAMETER Password + Required password that will be used for the security key in SSISDB. + + .PARAMETER SsisCatalog + SSIS catalog name. By default, this is SSISDB. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SSIS, SSISDB, Catalog + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Tags: + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSsisCatalog + + .EXAMPLE + $password = ConvertTo-SecureString MyVisiblePassWord -AsPlainText -Force + New-DbaSsisCatalog -SqlInstance sql2016 -Password $password + + Creates the SSIS Catalog on server DEV01 with the specified password. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt "Enter password" + New-DbaSsisCatalog -SqlInstance DEV01 -Password $password + + Creates the SSIS Catalog on server DEV01 with the specified password. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [Security.SecureString]$Password, + [string]$SsisCatalog = "SSISDB", + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + ## check if SSIS and Engine running on box + $services = Get-DbaSqlService -ComputerName $server.NetName + + $ssisservice = $Services | Where-Object { $_.ServiceType -eq "SSIS" -and $_.State -eq "Running" } + + if (-not $ssisservice) { + Stop-Function -Message "SSIS is not running on $instance" -Continue -Target $instance + } + + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + $clrenabled = Get-DbaSpConfigure -SqlInstance $server -Config IsSqlClrEnabled + + if (!$clrenabled.RunningValue) { + Stop-Function -Message 'CLR Integration must be enabled. You can enable it by running Set-DbaSpConfigure -SqlInstance sql2012 -Config IsSqlClrEnabled -Value $true' -Continue -Target $instance + } + + try { + $ssis = New-Object Microsoft.SqlServer.Management.IntegrationServices.IntegrationServices $server + } + catch { + Stop-Function -Message "Can't load server" -Target $instance -ErrorRecord $_ + return + } + + if ($ssis.Catalogs[$SsisCatalog]) { + Stop-Function -Message "SSIS Catalog already exists" -Continue -Target $ssis.Catalogs[$SsisCatalog] + } + else { + if ($Pscmdlet.ShouldProcess($server, "Creating SSIS catalog: $SsisCatalog")) { + try { + $ssisdb = New-Object Microsoft.SqlServer.Management.IntegrationServices.Catalog ($ssis, $SsisCatalog, $(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))))) + $ssisdb.Create() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + SsisCatalog = $SsisCatalog + Created = $true + } + } + catch { + Stop-Function -Message "Failed to create SSIS Catalog: $_" -Target $_ -Continue + } + } + } + } + } +} +function New-DbatoolsSupportPackage { + <# + .SYNOPSIS + Creates a package of troubleshooting information that can be used by dbatools to help debug issues. + + .DESCRIPTION + This function creates an extensive debugging package that can help with reproducing and fixing issues. + + The file will be created on the desktop by default and will contain quite a bit of information: + - OS Information + - Hardware Information (CPU, Ram, things like that) + - .NET Information + - PowerShell Information + - Your input history + - The In-Memory message log + - The In-Memory error log + - Screenshot of the console buffer (Basically, everything written in your current console, even if you have to scroll upwards to see it. + + .PARAMETER Path + The folder where to place the output xml in. + + .PARAMETER Variables + Name of additional variables to attach. + This allows you to add the content of variables to the support package, if you believe them to be relevant to the case. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Fred Weinmann (@FredWeinmann) + Tags: Debug + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbatoolsSupportPackage + + .EXAMPLE + New-DbatoolsSupportPackage + + Creates a large support pack in order to help us troubleshoot stuff. + #> + [CmdletBinding()] + param ( + [string] + $Path = "$($env:USERPROFILE)\Desktop", + + [string[]] + $Variables, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + #region Helper functions + function Get-ShellBuffer { + [CmdletBinding()] + Param () + + try { + # Define limits + $rec = New-Object System.Management.Automation.Host.Rectangle + $rec.Left = 0 + $rec.Right = $host.ui.rawui.BufferSize.Width - 1 + $rec.Top = 0 + $rec.Bottom = $host.ui.rawui.BufferSize.Height - 1 + + # Load buffer + $buffer = $host.ui.rawui.GetBufferContents($rec) + + # Convert Buffer to list of strings + $int = 0 + $lines = @() + while ($int -le $rec.Bottom) { + $n = 0 + $line = "" + while ($n -le $rec.Right) { + $line += $buffer[$int, $n].Character + $n++ + } + $line = $line.TrimEnd() + $lines += $line + $int++ + } + + # Measure empty lines at the beginning + $int = 0 + $temp = $lines[$int] + while ($temp -eq "") { $int++; $temp = $lines[$int] } + + # Measure empty lines at the end + $z = $rec.Bottom + $temp = $lines[$z] + while ($temp -eq "") { $z--; $temp = $lines[$z] } + + # Skip the line launching this very function + $z-- + + # Measure empty lines at the end (continued) + $temp = $lines[$z] + while ($temp -eq "") { $z--; $temp = $lines[$z] } + + # Cut results to the limit and return them + return $lines[$int .. $z] + } + catch { } + } + #endregion Helper functions + } + PROCESS { + $filePathXml = "$($Path.Trim('\'))\dbatools_support_pack_$(Get-Date -Format "yyyy_MM_dd-HH_mm_ss").xml" + $filePathZip = $filePathXml -replace "\.xml$", ".zip" + + Write-Message -Level Critical -Message @" +Gathering information... +Will write the final output to: $filePathZip + +Please submit this file to the team, to help with troubleshooting whatever issue you encountered. +Be aware that this package contains a lot of information including your input history in the console. +Please make sure no sensitive data (such as passwords) can be caught this way. + +Ideally start a new console, perform the minimal steps required to reproduce the issue, then run this command. +This will make it easier for us to troubleshoot and you won't be sending us the keys to your castle. +"@ + + $hash = @{ } + Write-Message -Level Output -Message "Collecting dbatools logged messages (Get-DbatoolsLog)" + $hash["Messages"] = Get-DbatoolsLog + Write-Message -Level Output -Message "Collecting dbatools logged errors (Get-DbatoolsLog -Errors)" + $hash["Errors"] = Get-DbatoolsLog -Errors + Write-Message -Level Output -Message "Collecting copy of console buffer (what you can see on your console)" + $hash["ConsoleBuffer"] = Get-ShellBuffer + Write-Message -Level Output -Message "Collecting Operating System information (Win32_OperatingSystem)" + $hash["OperatingSystem"] = Get-DbaCmObject -ClassName Win32_OperatingSystem + Write-Message -Level Output -Message "Collecting CPU information (Win32_Processor)" + $hash["CPU"] = Get-DbaCmObject -ClassName Win32_Processor + Write-Message -Level Output -Message "Collecting Ram information (Win32_PhysicalMemory)" + $hash["Ram"] = Get-DbaCmObject -ClassName Win32_PhysicalMemory + Write-Message -Level Output -Message "Collecting PowerShell & .NET Version (`$PSVersionTable)" + $hash["PSVersion"] = $PSVersionTable + Write-Message -Level Output -Message "Collecting Input history (Get-History)" + $hash["History"] = Get-History + Write-Message -Level Output -Message "Collecting list of loaded modules (Get-Module)" + $hash["Modules"] = Get-Module + Write-Message -Level Output -Message "Collecting list of loaded snapins (Get-PSSnapin)" + $hash["SnapIns"] = Get-PSSnapin + Write-Message -Level Output -Message "Collecting list of loaded assemblies (Name, Version, and Location)" + $hash["Assemblies"] = [appdomain]::CurrentDomain.GetAssemblies() | Select-Object CodeBase, FullName, Location, ImageRuntimeVersion, GlobalAssemblyCache, IsDynamic + + if (Test-Bound "Variables") { + Write-Message -Level Output -Message "Adding variables specified for export: $($Variables -join ", ")" + $hash["Variables"] = $Variables | Get-Variable -ErrorAction Ignore + } + + $data = [pscustomobject]$hash + + try { $data | Export-Clixml -Path $filePathXml -ErrorAction Stop } + catch { + Stop-Function -Message "Failed to export dump to file!" -ErrorRecord $_ -Target $filePathXml + return + } + + try { Compress-Archive -Path $filePathXml -DestinationPath $filePathZip -ErrorAction Stop } + catch { + Stop-Function -Message "Failed to pack dump-file into a zip archive. Please do so manually before submitting the results as the unpacked xml file will be rather large." -ErrorRecord $_ -Target $filePathZip + return + } + + Remove-Item -Path $filePathXml -ErrorAction Ignore + } + END { + Write-Message -Level InternalComment -Message "Ending" + } +} +function New-DbaXESession { + <# + .SYNOPSIS + Creates a new XESession object - for the dogged. + + .DESCRIPTION + Creates a new XESession object - for the dogged (very manual, Import-DbaXESession is recommended). See the following for more info: + + https://docs.microsoft.com/en-us/sql/relational-databases/extended-events/use-the-powershell-provider-for-extended-events + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The Name of the session to be created. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaXESession + + .EXAMPLE + $session = New-DbaXESession -SqlInstance sql2017 -Name XeSession_Test + $event = $session.AddEvent("sqlserver.file_written") + $event.AddAction("package0.callstack") + $session.Create() + + Returns a new XE Session object from sql2017 then adds an event, an action then creates it. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string]$Name, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + $store.CreateSession($Name) + } + } +} +function New-DbaXESmartCsvWriter { + <# + .SYNOPSIS + This Response type is used to write Extended Events to a CSV file. + + .DESCRIPTION + This Response type is used to write Extended Events to a CSV file. + + .PARAMETER OutputFile + Specifies the path to the output CSV file. + + .PARAMETER Overwrite + Specifies whether any existiting file should be overwritten or not. + + .PARAMETER OutputColumn + Specifies the list of columns to output from the events. XESmartTarget will capture in memory and write to the target table only the columns (fields or targets) that are present in this list. + + Fields and actions are matched in a case-sensitive manner. + + Expression columns are supported. Specify a column with ColumnName AS Expression to add an expression column (Example: Total AS Reads + Writes) + + .PARAMETER Event + Specifies a list of events to be processed (with others being ignored. By default, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartCsvWriter + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $columns = "cpu_time", "duration", "physical_reads", "logical_reads", "writes", "row_count" + $response = New-DbaXESmartCsvWriter -OutputFile c:\temp\workload.csv -OutputColumn $columns -OverWrite -Event "sql_batch_completed" + Start-DbaXESmartTarget -SqlInstance localhost\sql2017 -Session "Profiler Standard" -Responder $response + + Writes Extended Events to the file "C:\temp\workload.csv". + #> + [CmdletBinding()] + param ( + [parameter(Mandatory)] + [string]$OutputFile, + [switch]$Overwrite, + [string[]]$Event, + [string[]]$OutputColumn, + [string]$Filter, + [switch]$EnableException + ) + + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + try { + $writer = New-Object -TypeName XESmartTarget.Core.Responses.CsvAppenderResponse + $writer.OutputFile = $OutputFile + $writer.OverWrite = $Overwrite + if (Test-Bound -ParameterName "Event") { + $writer.Events = $Event + } + if (Test-Bound -ParameterName "OutputColumn") { + $writer.OutputColumns = $OutputColumn + } + if (Test-Bound -ParameterName "Filter") { + $writer.Filter = $Filter + } + $writer + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } +} +function New-DbaXESmartEmail { + <# + .SYNOPSIS + This Response type can be used to send an email each time an event is captured. + + .DESCRIPTION + This Response type can be used to send an email each time an event is captured. + + .PARAMETER SmtpServer + Address of the SMTP server for outgoing mail. + + .PARAMETER Sender + Sender's email address. + + .PARAMETER To + Address of the To recipient(s). + + .PARAMETER Cc + Address of the Cc recipient(s). + + .PARAMETER Bcc + Address of the Bcc recipient(s). + + .PARAMETER Credential + Credential object containing username and password used to authenticate on the SMTP server. When blank, no authentication is performed. + + .PARAMETER Subject + Subject of the mail message. Accepts placeholders in the text. + + Placeholders are in the form {PropertyName}, where PropertyName is one of the fields or actions available in the Event object. + + For instance, a valid Subject in a configuration file looks like this: "An event of name {Name} occurred at {collection_time}" + + .PARAMETER Body + Body of the mail message. The body can be static text or any property taken from the underlying event. See Subject for a description of how placeholders work. + + .PARAMETER Attachment + Data to attach to the email message. At this time, it can be any of the fields/actions of the underlying event. The data from the field/action is attached to the message as an ASCII stream. A single attachment is supported. + + .PARAMETER AttachmentFileName + File name to assign to the attachment. + + .PARAMETER PlainText + If this switch is enabled, the email will be sent in plain text. By default, HTML formatting is used. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + You can specify a filter expression by using this attribute. The filter expression is in the same form that you would use in a SQL query. For example, a valid example looks like this: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartEmail + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $params = @{ + SmtpServer = "smtp.ad.local" + To = "admin@ad.local" + Sender = "reports@ad.local" + Subject = "Query executed" + Body = "Query executed at {collection_time}" + Attachment = "batch_text" + AttachmentFileName = "query.sql" + } + $emailresponse = New-DbaXESmartEmail @params + Start-DbaXESmartTarget -SqlInstance sql2017 -Session querytracker -Responder $emailresponse + + Sends an email each time a querytracker event is captured. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory)] + [string]$SmtpServer, + [parameter(Mandatory)] + [string]$Sender, + [parameter(Mandatory)] + [string[]]$To, + [string[]]$Cc, + [string[]]$Bcc, + [pscredential]$Credential, + [parameter(Mandatory)] + [string]$Subject, + [parameter(Mandatory)] + [string]$Body, + [string]$Attachment, + [string]$AttachmentFileName, + [string]$PlainText, + [string[]]$Event, + [string]$Filter, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll." -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + try { + $email = New-Object -TypeName XESmartTarget.Core.Responses.EmailResponse + $email.SmtpServer = $SmtpServer + $email.Sender = $Sender + $email.To = $To + $email.Cc = $Cc + $email.Bcc = $Bcc + $email.Subject = $Subject + $email.Body = $Body + $email.Attachment = $Attachment + $email.AttachmentFileName = $AttachmentFileName + $email.HTMLFormat = ($PlainText -eq $false) + if (Test-Bound -ParameterName "Event") { + $email.Events = $Event + } + if (Test-Bound -ParameterName "Filter") { + $email.Filter = $Filter + } + + if ($Credential) { + $email.UserName = $Credential.UserName + $email.Password = $Credential.GetNetworkCredential().Password + } + + $email + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } +} +function New-DbaXESmartQueryExec { + <# + .SYNOPSIS + This Response type executes a T-SQL command against a target database whenever an event is recorded. + + .DESCRIPTION + This Response type executes a T-SQL command against a target database whenever an event is recorded. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database that contains the target table. + + .PARAMETER Query + The T-SQL command to execute. This string can contain placeholders for properties taken from the events. + + Placeholders are in the form {PropertyName}, where PropertyName is one of the fields or actions available in the Event object. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + You can specify a filter expression by using this attribute. The filter expression is in the same form that you would use in a SQL query. For example, a valid example looks like this: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaXESmartQueryExec + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $response = New-DbaXESmartQueryExec -SqlInstance sql2017 -Database dbadb -Query "update table set whatever = 1" + Start-DbaXESmartTarget -SqlInstance sql2017 -Session deadlock_tracker -Responder $response + + Executes a T-SQL command against dbadb on sql2017 whenever a deadlock event is recorded. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Database, + [string]$Query, + [switch]$EnableException, + [string[]]$Event, + [string]$Filter + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { + return + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $execute = New-Object -TypeName XESmartTarget.Core.Responses.ExecuteTSQLResponse + $execute.ServerName = $server.Name + $execute.DatabaseName = $Database + $execute.TSQL = $Query + + if ($SqlCredential) { + $execute.UserName = $SqlCredential.UserName + $execute.Password = $SqlCredential.GetNetworkCredential().Password + } + + if (Test-Bound -ParameterName "Event") { + $execute.Events = $Event + } + if (Test-Bound -ParameterName "Filter") { + $execute.Filter = $Filter + } + + $execute + } + } +} +function New-DbaXESmartReplay { + <# + .SYNOPSIS + This Response type can be used to replay execution related events to a target SQL Server instance. + + .DESCRIPTION + This Response type can be used to replay execution related events to a target SQL Server instance. The events that you can replay are of the type sql_batch_completed and rpc_completed: all other events are ignored. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Name of the initial catalog to connect to. Statements will be replayed by changing database to the same database where the event was originally captured, so this property only controls the initial database to connect to. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER DelaySeconds + Specifies the duration of the delay in seconds. + + .PARAMETER ReplayIntervalSeconds + Specifies the duration of the replay interval in seconds. + + .PARAMETER StopOnError + If this switch is enabled, the replay will be stopped when the first error is encountered. By default, error messages are piped to the log and console output, and replay proceeds. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartReplay + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $response = New-DbaXESmartReplay -SqlInstance sql2017 -Database planning + Start-DbaXESmartTarget -SqlInstance sql2016 -Session loadrelay -Responder $response + + Replays events from sql2016 on sql2017 in the planning database. Returns a PowerShell job object. + + To see a list of all SmartTarget job objects, use Get-DbaXESmartTarget. + + .EXAMPLE + $response = New-DbaXESmartReplay -SqlInstance sql2017 -Database planning + Start-DbaXESmartTarget -SqlInstance sql2017 -Session 'Profiler Standard' -Responder $response -NotAsJob + + Replays events from the 'Profiler Standard' session on sql2016 to sql2017's planning database. Does not run as a job so you can see the raw output. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Database, + [string[]]$Event = "sql_batch_completed", + [string]$Filter, + [int]$DelaySeconds, + [switch]$StopOnError, + [int]$ReplayIntervalSeconds, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $replay = New-Object -TypeName XESmartTarget.Core.Responses.ReplayResponse + $replay.ServerName = $instance + $replay.DatabaseName = $Database + $replay.Events = $Event + $replay.StopOnError = $StopOnError + $replay.Filter = $Filter + $replay.DelaySeconds = $DelaySeconds + $replay.ReplayIntervalSeconds = $ReplayIntervalSeconds + + if ($SqlCredential) { + $replay.UserName = $SqlCredential.UserName + $replay.Password = $SqlCredential.GetNetworkCredential().Password + } + + $replay + } + catch { + $message = $_.Exception.InnerException.InnerException | Out-String + Stop-Function -Message $message -Target "XESmartTarget" -Continue + } + } + } +} +function New-DbaXESmartTableWriter { + <# + .SYNOPSIS + This Response type is used to write Extended Events to a database table. + + .DESCRIPTION + This Response type is used to write Extended Events to a database table. The events are temporarily stored in memory before being written to the database at regular intervals. + + The target table can be created manually upfront or you can let the TableAppenderResponse create a target table based on the fields and actions available in the events captured. + + The columns of the target table and the fields/actions of the events are mapped by name (case-sensitive). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database that contains the target table. + + .PARAMETER Table + Specifies the name of the target table. + + .PARAMETER AutoCreateTargetTable + If this switch is enabled, XESmartTarget will infer the definition of the target table from the columns captured in the Extended Events session. + + If the target table already exists, it will not be recreated. + + .PARAMETER UploadIntervalSeconds + Specifies the number of seconds XESmartTarget will keep the events in memory before dumping them to the target table. The default is 10 seconds. + + .PARAMETER OutputColumn + Specifies the list of columns to output from the events. XESmartTarget will capture in memory and write to the target table only the columns (fields or targets) that are present in this list. + + Fields and actions are matched in a case-sensitive manner. + + Expression columns are supported. Specify a column with ColumnName AS Expression to add an expression column (Example: Total AS Reads + Writes) + + .PARAMETER Event + Specifies a list of events to be processed (with others being ignored. By default, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartTableWriter + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $columns = "cpu_time", "duration", "physical_reads", "logical_reads", "writes", "row_count", "batch_text" + $response = New-DbaXESmartTableWriter -SqlInstance sql2017 -Database dbadb -Table deadlocktracker -OutputColumn $columns -Filter "duration > 10000" + Start-DbaXESmartTarget -SqlInstance sql2017 -Session deadlock_tracker -Responder $response + + Writes Extended Events to the deadlocktracker table in dbadb on sql2017. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string]$Database, + [parameter(Mandatory)] + [string]$Table, + [switch]$AutoCreateTargetTable, + [int]$UploadIntervalSeconds = 10, + [string[]]$Event, + [string[]]$OutputColumn, + [string]$Filter, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $writer = New-Object -TypeName XESmartTarget.Core.Responses.TableAppenderResponse + $writer.ServerName = $server.Name + $writer.DatabaseName = $Database + $writer.TableName = $Table + $writer.AutoCreateTargetTable = $AutoCreateTargetTable + $writer.UploadIntervalSeconds = $UploadIntervalSeconds + if (Test-Bound -ParameterName "Event") { + $writer.Events = $Event + } + if (Test-Bound -ParameterName "OutputColumn") { + $writer.OutputColumns = $OutputColumn + } + if (Test-Bound -ParameterName "Filter") { + $writer.Filter = $Filter + } + $writer + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } + } +} +function Publish-DbaDacpac { + <# + .SYNOPSIS + The Publish-Database command takes a dacpac which is the output from an SSDT project and publishes it to a database. Changing the schema to match the dacpac and also to run any scripts in the dacpac (pre/post deploy scripts). + + .DESCRIPTION + Deploying a dacpac uses the DacFx which historically needed to be installed on a machine prior to use. In 2016 the DacFx was supplied by Microsoft as a nuget package and this uses that nuget package. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the filesystem path to the DACPAC + + .PARAMETER PublishXml + Specifies the publish profile which will include options and sqlCmdVariables. + + .PARAMETER Database + Specifies the name of the database being published. + + .PARAMETER ConnectionString + Specifies the connection string to the database you are upgrading. This is not required if SqlInstance is specified. + + .PARAMETER GenerateDeploymentScript + If this switch is enabled, the publish script will be generated. + + .PARAMETER GenerateDeploymentReport + If this switch is enabled, the publish XML report will be generated. + + .PARAMETER OutputPath + Specifies the filesystem path (directory) where output files will be generated. + + .PARAMETER ScriptOnly + If this switch is enabled, only the change scripts will be generated. + + .PARAMETER IncludeSqlCmdVars + If this switch is enabled, SqlCmdVars in publish.xml will have their values overwritten. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER DacFxPath + Path to the dac dll. If this is ommited, then the version of dac dll which is packaged with dbatools is used. + + .NOTES + Tags: Migration, Database, Dacpac + Author: Richie lee (@bzzzt_io) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Publish-DbaDacpac + + .EXAMPLE + Publish-DbaDacpac -SqlInstance sql2017 -Database WideWorldImporters -Path C:\temp\sql2016-WideWorldImporters.dacpac -PublishXml C:\temp\sql2016-WideWorldImporters-publish.xml + + Updates WideWorldImporters on sql2017 from the sql2016-WideWorldImporters.dacpac using the sql2016-WideWorldImporters-publish.xml publish profile + + .EXAMPLE + New-DbaPublishProfile -SqlInstance sql2016 -Database db2 -Path C:\temp + Export-DbaDacpac -SqlInstance sql2016 -Database db2 | Publish-DbaDacpac -PublishXml C:\temp\sql2016-db2-publish.xml -Database db1, db2 -SqlInstance sql2017 + + Creates a publish profile at C:\temp\sql2016-db2-publish.xml, exports the .dacpac to $home\Documents\sql2016-db2.dacpac + then publishes it to the sql2017 server database db2 + + .EXAMPLE + $loc = "C:\Users\bob\source\repos\Microsoft.Data.Tools.Msbuild\lib\net46\Microsoft.SqlServer.Dac.dll" + Publish-DbaDacpac -SqlInstance "local" -Database WideWorldImporters -Path C:\temp\WideWorldImporters.dacpac -PublishXml C:\temp\WideWorldImporters.publish.xml -DacFxPath $loc + #> + [CmdletBinding()] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$Path, + [Parameter(Mandatory)] + [string]$PublishXml, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string[]]$Database, + [string[]]$ConnectionString, + [switch]$GenerateDeploymentScript, + [switch]$GenerateDeploymentReport, + [Switch]$ScriptOnly, + [string]$OutputPath = "$home\Documents", + [switch]$IncludeSqlCmdVars, + [switch]$EnableException, + [String]$DacFxPath + ) + + begin { + if ((Test-Bound -Not -ParameterName SqlInstance) -and (Test-Bound -Not -ParameterName ConnectionString)) { + Stop-Function -Message "You must specify either SqlInstance or ConnectionString." + } + if ((Test-Bound -ParameterName GenerateDeploymentScript) -or (Test-Bound -ParameterName GenerateDeploymentReport)) { + $defaultcolumns = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Dacpac', 'PublishXml', 'Result', 'DatabaseScriptPath', 'MasterDbScriptPath', 'DeploymentReport', 'DeployOptions', 'SqlCmdVariableValues' + } + else { + $defaultcolumns = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Dacpac', 'PublishXml', 'Result', 'DeployOptions', 'SqlCmdVariableValues' + } + if ((Test-Bound -ParameterName ScriptOnly) -and (Test-Bound -Not -ParameterName GenerateDeploymentScript) -and (Test-Bound -Not -ParameterName GenerateDeploymentScript)) { + Stop-Function -Message "You must at least one of GenerateDeploymentScript or GenerateDeploymentReport when using ScriptOnly" + } + + function Get-ServerName ($connstring) { + $builder = New-Object System.Data.Common.DbConnectionStringBuilder + $builder.set_ConnectionString($connstring) + $instance = $builder['data source'] + + if (-not $instance) { + $instance = $builder['server'] + } + + return $instance.ToString().Replace('\', '-').Replace('(','').Replace(')','') + } + if (Test-Bound -Not -ParameterName 'DacfxPath'){ + $dacfxPath = "$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Dac.dll" + } + + if ((Test-Path $dacfxPath) -eq $false) { + Stop-Function -Message 'No usable version of Dac Fx found.' -EnableException $EnableException + } + else { + try { + Add-Type -Path $dacfxPath + Write-Message -Level Verbose -Message "Dac Fx loaded." + } + catch { + Stop-Function -Message 'No usable version of Dac Fx found.' -EnableException $EnableException -ErrorRecord $_ + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + if (-not (Test-Path -Path $Path)) { + Stop-Function -Message "$Path not found!" + } + + if (-not (Test-Path -Path $PublishXml)) { + Stop-Function -Message "$PublishXml not found!" + } + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $ConnectionString += $server.ConnectionContext.ConnectionString.Replace('"', "'") + } + + try { + $dacPackage = [Microsoft.SqlServer.Dac.DacPackage]::Load($Path) + } + catch { + Stop-Function -Message "Could not load package." -ErrorRecord $_ + } + + try { + $dacProfile = [Microsoft.SqlServer.Dac.DacProfile]::Load($PublishXml) + } + catch { + Stop-Function -Message "Could not load profile." -ErrorRecord $_ + } + + if ($IncludeSqlCmdVars) { + Get-SqlCmdVars -SqlCommandVariableValues $dacProfile.DeployOptions.SqlCommandVariableValues + } + + foreach ($connstring in $ConnectionString) { + $cleaninstance = Get-ServerName $connstring + $instance = $cleaninstance.ToString().Replace('--', '\') + + foreach ($dbname in $database) { + if ($GenerateDeploymentScript -or $GenerateDeploymentReport) { + $timeStamp = (Get-Date).ToString("yyMMdd_HHmmss_f") + $DatabaseScriptPath = Join-Path $OutputPath "$cleaninstance-$dbname`_DeployScript_$timeStamp.sql" + $MasterDbScriptPath = Join-Path $OutputPath "$cleaninstance-$dbname`_Master.DeployScript_$timeStamp.sql" + $DeploymentReport = Join-Path $OutputPath "$cleaninstance-$dbname`_Result.DeploymentReport_$timeStamp.xml" + } + + if ($connstring -notmatch 'Database=') { + $connstring = "$connstring;Database=$dbname" + } + + try { + $dacServices = New-Object Microsoft.SqlServer.Dac.DacServices $connstring + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server -Continue + } + + $options = @{ + GenerateDeploymentScript = $GenerateDeploymentScript + GenerateDeploymentReport = $GenerateDeploymentReport + DatabaseScriptPath = $DatabaseScriptPath + MasterDbScriptPath = $MasterDbScriptPath + DeployOptions = $dacProfile.DeployOptions + } + + try { + $global:output = @() + Register-ObjectEvent -InputObject $dacServices -EventName "Message" -SourceIdentifier "msg" -Action { $global:output += $EventArgs.Message.Message } | Out-Null + if ($ScriptOnly) { + Write-Message -Level Verbose -Message "Generating script." + $result = $dacServices.Script($dacPackage, $dbname, $options) + } + else { + Write-Message -Level Verbose -Message "Executing Deployment." + $result = $dacServices.Publish($dacPackage, $dbname, $options) + } + } + catch [Microsoft.SqlServer.Dac.DacServicesException] { + Stop-Function -Message "Deployment failed" -ErrorRecord $_ -EnableException $true + } + finally { + Unregister-Event -SourceIdentifier "msg" + if ($GenerateDeploymentReport) { + $result.DeploymentReport | Out-File $DeploymentReport + Write-Message -Level Verbose -Message "Deployment Report - $DeploymentReport." + } + if ($GenerateDeploymentScript) { + Write-Message -Level Verbose -Message "Database change script - $DatabaseScriptPath." + if ((Test-Path $MasterDbScriptPath)) { + Write-Message -Level Verbose -Message "Master database change script - $($result.MasterDbScript)." + } + } + $resultoutput = ($global:output -join "`r`n" | Out-String).Trim() + if ($resultoutput -match "Failed" -and ($GenerateDeploymentReport -or $GenerateDeploymentScript)) { + Write-Message -Level Warning -Message "Seems like the attempt to publish/script may have failed. If scripts have not generated load dacpac into Visual Studio to check SQL is valid." + } + $server = [dbainstance]$instance + $deployOptions = $dacProfile.DeployOptions | Select-Object -Property * -ExcludeProperty "SqlCommandVariableValues" + [pscustomobject]@{ + ComputerName = $server.ComputerName + InstanceName = $server.InstanceName + SqlInstance = $server.FullName + Database = $dbname + Result = $resultoutput + Dacpac = $Path + PublishXml = $PublishXml + ConnectionString = $connstring + DatabaseScriptPath = $DatabaseScriptPath + MasterDbScriptPath = $MasterDbScriptPath + DeploymentReport = $DeploymentReport + DeployOptions = $deployOptions + SqlCmdVariableValues = $dacProfile.DeployOptions.SqlCommandVariableValues.Keys + + } | Select-DefaultView -Property $defaultcolumns + } + } + } + } +} +function Read-DbaAuditFile { + <# + .SYNOPSIS + Read Audit details from a sqlaudit file. + + .DESCRIPTION + Read Audit details from a sqlaudit file. + + .PARAMETER Path + The path to the sqlaudit file. This is relative to the computer executing the command. UNC paths are supported. + + .PARAMETER Exact + If this switch is enabled, only an exact search will be used for the Path. By default, this command will add a wildcard to the Path because Eventing uses the file name as a template and adds characters. + + .PARAMETER Raw + If this switch is enabled, the Microsoft.SqlServer.XEvent.Linq.PublishedEvent enumeration object will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, Audit + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaAuditFile + + .EXAMPLE + Read-DbaAuditFile -Path C:\temp\logins.sqlaudit + + Returns events from C:\temp\logins.sqlaudit. + + .EXAMPLE + Get-ChildItem C:\temp\audit\*.sqlaudit | Read-DbaAuditFile + + Returns events from all .sqlaudit files in C:\temp\audit. + + .EXAMPLE + Get-DbaServerAudit -SqlInstance sql2014 -Audit LoginTracker | Read-DbaAuditFile + + Reads remote Audit details by accessing the file over the admin UNC share. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('FullName')] + [object[]]$Path, + [switch]$Exact, + [switch]$Raw, + [switch]$EnableException + ) + process { + foreach ($file in $path) { + # in order to ensure CSV gets all fields, all columns will be + # collected and output in the first (all all subsequent) object + $columns = @("name", "timestamp") + + if ($file -is [System.String]) { + $currentfile = $file + $manualadd = $true + } + elseif ($file -is [System.IO.FileInfo]) { + $currentfile = $file.FullName + $manualadd = $true + } + else { + if ($file -isnot [Microsoft.SqlServer.Management.Smo.Audit]) { + Stop-Function -Message "Unsupported file type." + return + } + + if ($file.FullName.Length -eq 0) { + Stop-Function -Message "This Audit does not have an associated file." + return + } + + $instance = [dbainstance]$file.ComputerName + + if ($instance.IsLocalHost) { + $currentfile = $file.FullName + } + else { + $currentfile = $file.RemoteFullName + } + } + + if (-not $Exact) { + $currentfile = $currentfile.Replace('.sqlaudit', '*.sqlaudit') + + if ($currentfile -notmatch "sqlaudit") { + $currentfile = "$currentfile*.sqlaudit" + } + } + + $accessible = Test-Path -Path $currentfile + $whoami = whoami + + if (-not $accessible) { + if ($file.Status -eq "Stopped") { continue } + Stop-Function -Continue -Message "$currentfile cannot be accessed from $($env:COMPUTERNAME). Does $whoami have access?" + } + + if ($raw) { + return New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + } + + $enum = New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + $newcolumns = ($enum.Fields.Name | Select-Object -Unique) + + $actions = ($enum.Actions.Name | Select-Object -Unique) + foreach ($action in $actions) { + $newcolumns += ($action -Split '\.')[-1] + } + + $newcolumns = $newcolumns | Sort-Object + $columns = ($columns += $newcolumns) | Select-Object -Unique + + # Make it selectable, otherwise it's a weird enumeration + foreach ($event in (New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile))) { + $hash = [ordered]@{ } + + foreach ($column in $columns) { + $null = $hash.Add($column, $event.$column) + } + + foreach ($action in $event.Actions) { + $hash[$action.Name] = $action.Value + } + + foreach ($field in $event.Fields) { + $hash[$field.Name] = $field.Value + } + + [pscustomobject]$hash + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Read-DbaBackupHeader { + <# + .SYNOPSIS + Reads and displays detailed information about a SQL Server backup. + + .DESCRIPTION + Reads full, differential and transaction log backups. An online SQL Server is required to parse the backup files and the path specified must be relative to that SQL Server. + + .PARAMETER SqlInstance + The SQL Server instance to use for parsing the backup files. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Path to SQL Server backup file. This can be a full, differential or log backup file. Accepts valid filesystem paths and URLs. + + .PARAMETER Simple + If this switch is enabled, fewer columns are returned, giving an easy overview. + + .PARAMETER FileList + If this switch is enabled, detailed information about the files within the backup is returned. + + .PARAMETER AzureCredential + Name of the SQL Server credential that should be used for Azure storage access. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaBackupHeader + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance sql2016 -Path S:\backups\mydb\mydb.bak + + Logs into sql2016 using Windows authentication and reads the local file on sql2016, S:\backups\mydb\mydb.bak. + + If you are running this command on a workstation and connecting remotely, remember that sql2016 cannot access files on your own workstation. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance sql2016 -Path \\nas\sql\backups\mydb\mydb.bak, \\nas\sql\backups\otherdb\otherdb.bak + + Logs into sql2016 and reads two backup files - mydb.bak and otherdb.bak. The SQL Server service account must have rights to read this file. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak -Simple + + Logs into the local workstation (or computer) and shows simplified output about C:\temp\myfile.bak. The SQL Server service account must have rights to read this file. + + .EXAMPLE + $backupinfo = Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak + $backupinfo.FileList + + Displays detailed information about each of the datafiles contained in the backupset. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak -FileList + + Also returns detailed information about each of the datafiles contained in the backupset. + + .EXAMPLE + "C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" | Read-DbaBackupHeader -SqlInstance sql2016 + + Similar to running Read-DbaBackupHeader -SqlInstance sql2016 -Path "C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" + + .EXAMPLE + Get-ChildItem \\nas\sql\*.bak | Read-DbaBackupHeader -SqlInstance sql2016 + + Gets a list of all .bak files on the \\nas\sql share and reads the headers using the server named "sql2016". This means that the server, sql2016, must have read access to the \\nas\sql share. + + .EXAMPLE + Read-DbaBackupHeader -Path https://dbatoolsaz.blob.core.windows.net/azbackups/restoretime/restoretime_201705131850.bak + -AzureCredential AzureBackupUser + + Gets the backup header information from the SQL Server backup file stored at https://dbatoolsaz.blob.core.windows.net/azbackups/restoretime/restoretime_201705131850.bak on Azure + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", '')] + <# AzureCredential is utilized in this command is not a formal Credential object. #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Path, + [switch]$Simple, + [switch]$FileList, + [string]$AzureCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + foreach($p in $path) { + if ([System.IO.Path]::GetExtension($p).Length -eq 0) { + Stop-Function -Message "Path ($p) should be a file, not a folder" -Category InvalidArgument + return + } + } + Write-Message -Level InternalComment -Message "Starting reading headers" + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + $getHeaderScript = { + Param ( + $SqlInstance, + $Path, + $DeviceType, + $AzureCredential + ) + #Copy existing connection to create an independent TSQL session + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $SqlInstance.ConnectionContext.Copy() + $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + + if ($DeviceType -eq 'URL') { + $restore.CredentialName = $AzureCredential + } + + $device = New-Object Microsoft.SqlServer.Management.Smo.BackupDeviceItem $Path, $DeviceType + $restore.Devices.Add($device) + $dataTable = $restore.ReadBackupHeader($server) + + $null = $dataTable.Columns.Add("FileList", [object]) + + $mb = $dataTable.Columns.Add("BackupSizeMB", [int]) + $mb.Expression = "BackupSize / 1024 / 1024" + $gb = $dataTable.Columns.Add("BackupSizeGB") + $gb.Expression = "BackupSizeMB / 1024" + + if ($null -eq $dataTable.Columns['CompressedBackupSize']) { + $formula = "0" + } + else { + $formula = "CompressedBackupSize / 1024 / 1024" + } + + $cmb = $dataTable.Columns.Add("CompressedBackupSizeMB", [int]) + $cmb.Expression = $formula + $cgb = $dataTable.Columns.Add("CompressedBackupSizeGB") + $cgb.Expression = "CompressedBackupSizeMB / 1024" + + $null = $dataTable.Columns.Add("SqlVersion") + + $null = $dataTable.Columns.Add("BackupPath") + + foreach ($row in $dataTable) { + $row.BackupPath = $Path + $restore.FileNumber = $row.Position + <# Select-Object does a quick and dirty conversion from datatable to PS object #> + $row.FileList = $restore.ReadFileList($server) | Select-Object * + } + $dataTable + } + } + + process { + if (Test-FunctionInterrupt) { return } + + #Extract fullnames from the file system objects + $pathStrings = @() + foreach ($pathItem in $Path) { + if ($null -ne $pathItem.FullName) { + $pathStrings += $pathItem.FullName + } + else { + $pathStrings += $pathItem + } + } + #Group by filename + $pathGroup = $pathStrings | Group-Object -NoElement | Select-Object -ExpandProperty Name + + $pathCount = ($pathGroup | Measure-Object).Count + Write-Message -Level Verbose -Message "$pathCount unique files to scan." + Write-Message -Level Verbose -Message "Checking accessibility for all the files." + + $testPath = Test-DbaSqlPath -SqlInstance $server -Path $pathGroup + + #Setup initial session state + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + #Create Runspace pool, min - 1, max - 10 sessions: there is internal SQL Server queue for the restore operations. 10 threads seem to perform best + $runspacePool = [runspacefactory]::CreateRunspacePool(1, 10, $InitialSessionState, $Host) + $runspacePool.Open() + + $threads = @() + + foreach ($file in $pathGroup) { + if ($file -like 'http*') { + $deviceType = 'URL' + } + else { + $deviceType = 'FILE' + } + if ($pathCount -eq 1) { + $fileExists = $testPath + } + else { + $fileExists = ($testPath | Where-Object FilePath -eq $file).FileExists + } + if ($fileExists -or $deviceType -eq 'URL') { + #Create parameters hashtable + $argsRunPool = @{ + SqlInstance = $server + Path = $file + AzureCredential = $AzureCredential + DeviceType = $deviceType + } + Write-Message -Level Verbose -Message "Scanning file $file." + #Create new runspace thread + $thread = [powershell]::Create() + $thread.RunspacePool = $runspacePool + $thread.AddScript($getHeaderScript) | Out-Null + $thread.AddParameters($argsRunPool) | Out-Null + #Start the thread + $handle = $thread.BeginInvoke() + $threads += [pscustomobject]@{ + handle = $handle + thread = $thread + file = $file + deviceType = $deviceType + isRetrieved = $false + started = Get-Date + } + } + else { + Write-Message -Level Warning -Message "File $file does not exist or access denied. The SQL Server service account may not have access to the source directory." + } + } + #receive runspaces + while ($threads | Where-Object { $_.isRetrieved -eq $false }) { + $totalThreads = ($threads | Measure-Object).Count + $totalRetrievedThreads = ($threads | Where-Object { $_.isRetrieved -eq $true } | Measure-Object).Count + Write-Progress -Id 1 -Activity Updating -Status 'Progress' -CurrentOperation "Scanning Restore headers: $totalRetrievedThreads/$totalThreads" -PercentComplete ($totalRetrievedThreads / $totalThreads * 100) + foreach ($thread in ($threads | Where-Object { $_.isRetrieved -eq $false })) { + if ($thread.Handle.IsCompleted) { + $dataTable = $thread.thread.EndInvoke($thread.handle) + $thread.isRetrieved = $true + #Check if thread had any errors + if ($thread.thread.HadErrors) { + if ($thread.deviceType -eq 'FILE') { + Stop-Function -Message "Problem found with $($thread.file)." -Target $thread.file -ErrorRecord $thread.thread.Streams.Error -Continue + } + else { + Stop-Function -Message "Unable to read $($thread.file), check credential $AzureCredential and network connectivity." -Target $thread.file -ErrorRecord $thread.thread.Streams.Error -Continue + } + } + #Process the result of this thread + + $dbVersion = $dataTable[0].DatabaseVersion + $SqlVersion = (Convert-DbVersionToSqlVersion $dbVersion) + foreach ($row in $dataTable) { + $row.SqlVersion = $SqlVersion + if ($row.BackupName -eq "*** INCOMPLETE ***") { + Stop-Function -Message "$($thread.file) appears to be from a new version of SQL Server than $SqlInstance, skipping" -Target $thread.file -Continue + } + } + if ($Simple) { + $dataTable | Select-Object DatabaseName, BackupFinishDate, RecoveryModel, BackupSizeMB, CompressedBackupSizeMB, DatabaseCreationDate, UserName, ServerName, SqlVersion, BackupPath + } + elseif ($FileList) { + $dataTable.filelist + } + else { + $dataTable + } + + $thread.thread.Dispose() + } + } + Start-Sleep -Milliseconds 500 + } + #Close the runspace pool + $runspacePool.Close() + } +} +function Read-DbaTraceFile { + <# + .SYNOPSIS + Reads SQL Server trace files + + .DESCRIPTION + Using the fn_trace_gettable function, a trace file is read and returned as a PowerShell object + + This function returns the whole of the trace file. The information is presented in the format that the trace subsystem uses. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Path to the trace file. This path is relative to the SQL Server instance. + + .PARAMETER Database + Search for results only with specific DatabaseName. Uses IN for comparisons. + + .PARAMETER Login + Search for results only with specific Logins. Uses IN for comparisons. + + .PARAMETER Spid + Search for results only with specific Spids. Uses IN for comparisons. + + .PARAMETER EventClass + Search for results only with specific EventClasses. Uses IN for comparisons. + + .PARAMETER ObjectType + Search for results only with specific ObjectTypes. Uses IN for comparisons. + + .PARAMETER Error + Search for results only with specific Errors. Uses IN for comparisons. + + .PARAMETER EventSequence + Search for results only with specific EventSequences. Uses IN for comparisons. + + .PARAMETER TextData + Search for results only with specific TextData. Uses LIKE for comparisons. + + .PARAMETER ApplicationName + Search for results only with specific ApplicationNames. Uses LIKE for comparisons. + + .PARAMETER ObjectName + Search for results only with specific ObjectNames. Uses LIKE for comparisons. + + .PARAMETER Where + Custom where clause - use without the word "WHERE". Here are the available columns: + + TextData + BinaryData + DatabaseID + TransactionID + LineNumber + NTUserName + NTDomainName + HostName + ClientProcessID + ApplicationName + LoginName + SPID + Duration + StartTime + EndTime + Reads + Writes + CPU + Permissions + Severity + EventSubClass + ObjectID + Success + IndexID + IntegerData + ServerName + EventClass + ObjectType + NestLevel + State + Error + Mode + Handle + ObjectName + DatabaseName + FileName + OwnerName + RoleName + TargetUserName + DBUserName + LoginSid + TargetLoginName + TargetLoginSid + ColumnPermissions + LinkedServerName + ProviderName + MethodName + RowCounts + RequestID + XactSequence + EventSequence + BigintData1 + BigintData2 + GUID + IntegerData2 + ObjectID2 + Type + OwnerID + ParentName + IsSystem + Offset + SourceDatabaseID + SqlHandle + SessionLoginName + PlanHandle + GroupID + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Database master, tempdb -Path C:\traces\big.trc + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. Filters only results that have master or tempdb as the DatabaseName. + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Database master, tempdb -Path C:\traces\big.trc -TextData 'EXEC SP_PROCOPTION' + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. + Filters only results that have master or tempdb as the DatabaseName and that have 'EXEC SP_PROCOPTION' somewhere in the text. + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Path C:\traces\big.trc -Where "LinkedServerName = 'myls' and StartTime > '5/30/2017 4:27:52 PM'" + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. + Filters only results where LinkServerName = myls and StartTime is greater than '5/30/2017 4:27:52 PM'. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2014 | Read-DbaTraceFile + + Reads every trace file on sql2014 + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(ValueFromPipelineByPropertyName)] + [PSCredential]$SqlCredential, + [parameter(ValueFromPipelineByPropertyName)] + [string[]]$Path, + [string[]]$Database, + [string[]]$Login, + [int[]]$Spid, + [string[]]$EventClass, + [string[]]$ObjectType, + [int[]]$Error, + [int[]]$EventSequence, + [string[]]$TextData, + [string[]]$ApplicationName, + [string[]]$ObjectName, + [string]$Where, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($where) { + $Where = "where $where" + } + elseif ($Database -or $Login -or $Spid -or $ApplicationName -or $EventClass -or $ObjectName -or $ObjectType -or $EventSequence -or $Error) { + + $tempwhere = @() + + if ($Database) { + $where = $database -join "','" + $tempwhere += "databasename in ('$where')" + } + + if ($Login) { + $where = $Login -join "','" + $tempwhere += "LoginName in ('$where')" + } + + if ($Spid) { + $where = $Spid -join "," + $tempwhere += "Spid in ($where)" + } + + if ($EventClass) { + $where = $EventClass -join "," + $tempwhere += "EventClass in ($where)" + } + + if ($ObjectType) { + $where = $ObjectType -join "," + $tempwhere += "ObjectType in ($where)" + } + + if ($Error) { + $where = $Error -join "," + $tempwhere += "Error in ($where)" + } + + if ($EventSequence) { + $where = $EventSequence -join "," + $tempwhere += "EventSequence in ($where)" + } + + if ($TextData) { + $where = $TextData -join "%','%" + $tempwhere += "TextData like ('%$where%')" + } + + if ($ApplicationName) { + $where = $ApplicationName -join "%','%" + $tempwhere += "ApplicationName like ('%$where%')" + } + + if ($ObjectName) { + $where = $ObjectName -join "%','%" + $tempwhere += "ObjectName like ('%$where%')" + } + + $tempwhere = $tempwhere -join " and " + $Where = "where $tempwhere" + } + } + process { + foreach ($instance in $sqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + if (Test-Bound -Parameter Path) { + $currentpath = $path + } + else { + $currentpath = $server.ConnectionContext.ExecuteScalar("Select path from sys.traces where is_default = 1") + } + + foreach ($file in $currentpath) { + Write-Message -Level Verbose -Message "Parsing $file" + + $exists = Test-DbaSqlPath -SqlInstance $server -Path $file + + if (!$exists) { + Write-Message -Level Warning -Message "Path does not exist" -Target $file + Continue + } + + $sql = "select SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + * FROM [fn_trace_gettable]('$file', DEFAULT) $Where" + + try { + $server.Query($sql) + } + catch { + Stop-Function -Message "Error returned from SQL Server: $_" -Target $server -InnerErrorRecord $_ + } + } + } + } +} +function Read-DbaTransactionLog { + <# +.SYNOPSIS +Reads the live Transaction log from specified SQL Server Database + +.DESCRIPTION +Using the fn_dblog function, the live transaction log is read and returned as a PowerShell object + +This function returns the whole of the log. The information is presented in the format that the logging subsystem uses. + +A soft limit of 0.5GB of log as been implemented. This is based on testing. This limit can be overridden +at the users request, but please be aware that this may have an impact on your target databases and on the +system running this function + +.PARAMETER SqlInstance +A SQL Server instance to connect to + +.PARAMETER SqlCredential +A credential to use to connect to the SQL Instance rather than using Windows Authentication + +.PARAMETER Database +Database to read the transaction log of + +.PARAMETER IgnoreLimit +Switch to indicate that you wish to bypass the recommended limits of the function + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database, Log, LogFile +Author: Stuart Moore (@napalmgram), stuart-moore.com + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +$Log = Read-DbaTransactionLog -SqlInstance sql2016 -Database MyDatabase + +Will read the contents of the transaction log of MyDatabase on SQL Server Instance sql2016 into the local PowerShell object $Log + +.EXAMPLE +$Log = Read-DbaTransactionLog -SqlInstance sql2016 -Database MyDatabase -IgnoreLimit + +Will read the contents of the transaction log of MyDatabase on SQL Server Instance sql2016 into the local PowerShell object $Log, ignoring the recommnedation of not returning more that 0.5GB of log + +#> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Position = 0, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [object]$Database, + [Switch]$IgnoreLimit, + [Alias('Silent')] + [switch]$EnableException + ) + + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + if (-not $server.databases[$Database]) { + Stop-Function -Message "$Database does not exist" + return + } + + if ($server.databases[$Database].Status -ne 'Normal') { + Stop-Function -Message "$Database is not in a normal State, command will not run." + return + } + + if ($IgnoreLimit) { + Write-Message -Level Verbose -Message "Please be aware that ignoring the recommended limits may impact on the performance of the SQL Server database and the calling system" + } + else { + #Warn if more than 0.5GB of live log. Dodgy conversion as SMO returns the value in an unhelpful format :( + $SqlSizeCheck = "select + sum(FileProperty(sf.name,'spaceused')*8/1024) as 'SizeMb' + from sys.sysfiles sf + where CONVERT(INT,sf.status & 0x40) / 64=1" + $TransLogSize = $server.Query($SqlSizeCheck, $Database) + if ($TransLogSize.SizeMb -ge 500) { + Stop-Function -Message "$Database has more than 0.5 Gb of live log data, returning this may have an impact on the database and the calling system. If you wish to proceed please rerun with the -IgnoreLimit switch" + return + } + } + + $sql = "select * from fn_dblog(NULL,NULL)" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Starting Log retrieval" + $server.Query($sql, $Database) + +} +function Read-DbaXEFile { + <# + .SYNOPSIS + Read XEvents from a xel or xem file. + + .DESCRIPTION + Read XEvents from a xel or xem file. + + .PARAMETER Path + The path to the xel or xem file. This is relative to the computer executing the command. UNC paths are supported. + + .PARAMETER Exact + If this switch is enabled, only an exact search will be used for the Path. By default, this command will add a wildcard to the Path because Eventing uses the file name as a template and adds characters. + + .PARAMETER Raw + If this switch is enabled, the Microsoft.SqlServer.XEvent.Linq.PublishedEvent enumeration object will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaXEFile + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\deadocks.xel + + Returns events from C:\temp\deadocks.xel. + + .EXAMPLE + Get-ChildItem C:\temp\xe\*.xel | Read-DbaXEFile + + Returns events from all .xel files in C:\temp\xe. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2014 -Session deadlocks | Read-DbaXEFile + + Reads remote XEvents by accessing the file over the admin UNC share. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('FullName')] + [object[]]$Path, + [switch]$Exact, + [switch]$Raw, + [switch][Alias('Silent')] + $EnableException + ) + process { + foreach ($file in $path) { + # in order to ensure CSV gets all fields, all columns will be + # collected and output in the first (all all subsequent) object + $columns = @("name", "timestamp") + + if ($file -is [System.String]) { + $currentfile = $file + $manualadd = $true + } + elseif ($file -is [System.IO.FileInfo]) { + $currentfile = $file.FullName + $manualadd = $true + } + else { + if ($file -isnot [Microsoft.SqlServer.Management.XEvent.Session]) { + Stop-Function -Message "Unsupported file type." + return + } + + if ($file.TargetFile.Length -eq 0) { + Stop-Function -Message "This session does not have an associated Target File." + return + } + + $instance = [dbainstance]$file.ComputerName + + if ($instance.IsLocalHost) { + $currentfile = $file.TargetFile + } + else { + $currentfile = $file.RemoteTargetFile + } + } + + if (-not $Exact) { + $currentfile = $currentfile.Replace('.xel', '*.xel') + $currentfile = $currentfile.Replace('.xem', '*.xem') + + if ($currentfile -notmatch "xel" -and $currentfile -notmatch "xem") { + $currentfile = "$currentfile*.xel" + } + } + + $accessible = Test-Path -Path $currentfile + $whoami = whoami + + if (-not $accessible) { + if ($file.Status -eq "Stopped") { continue } + Stop-Function -Continue -Message "$currentfile cannot be accessed from $($env:COMPUTERNAME). Does $whoami have access?" + } + + if ($raw) { + return New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + } + + $enum = New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + $newcolumns = ($enum.Fields.Name | Select-Object -Unique) + + $actions = ($enum.Actions.Name | Select-Object -Unique) + foreach ($action in $actions) { + $newcolumns += ($action -Split '\.')[-1] + } + + $newcolumns = $newcolumns | Sort-Object + $columns = ($columns += $newcolumns) | Select-Object -Unique + + # Make it selectable, otherwise it's a weird enumeration + foreach ($event in (New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile))) { + $hash = [ordered]@{ } + + foreach ($column in $columns) { + $null = $hash.Add($column, $event.$column) + } + + foreach ($action in $event.Actions) { + $hash[$action.Name] = $action.Value + } + + foreach ($field in $event.Fields) { + $hash[$field.Name] = $field.Value + } + + [pscustomobject]$hash + } + } + } +} +function Register-DbaConfig { + <# + .SYNOPSIS + Registers an existing configuration object in registry. + + .DESCRIPTION + Registers an existing configuration object in registry. + This allows simple persisting of settings across powershell consoles. + It also can be used to generate a registry template, which can then be used to create policies. + + .PARAMETER Config + The configuration object to write to registry. + Can be retrieved using Get-DbaConfig. + + .PARAMETER FullName + The full name of the setting to be written to registry. + + .PARAMETER Module + The name of the module, whose settings should be written to registry. + + .PARAMETER Name + Default: "*" + Used in conjunction with the -Module parameter to restrict the number of configuration items written to registry. + + .PARAMETER Scope + Default: UserDefault + Who will be affected by this export how? Current user or all? Default setting or enforced? + Legal values: UserDefault, UserMandatory, SystemDefault, SystemMandatory + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Config, Module + Author: Friedrich Weinmann + + .EXAMPLE + PS C:\> Get-DbaConfig message.* | Register-DbaConfig + + Retrieves all configuration items that that start with message. and registers them in registry for the current user. + + .EXAMPLE + PS C:\> Register-DbaConfig -FullName "developer.mode.enable" -Scope SystemDefault + + Retrieves the configuration item "developer.mode.enable" and registers it in registry as the default setting for all users on this machine. + + .EXAMPLE + PS C:\> Register-DbaConfig -Module message -Scope SystemMandatory + + Retrieves all configuration items of the module MyModule, then registers them in registry to enforce them for all users on the current system. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Configuration.Config[]] + $Config, + + [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] + [string[]] + $FullName, + + [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)] + [string] + $Module, + + [Parameter(ParameterSetName = "Name", Position = 1)] + [string] + $Name = "*", + + [Sqlcollaborative.Dbatools.Configuration.ConfigScope] + $Scope = "UserDefault", + + [switch] + $EnableException + ) + + begin { + $parSet = $PSCmdlet.ParameterSetName + + function Write-Config { + [CmdletBinding()] + Param ( + [Sqlcollaborative.Dbatools.Configuration.Config] + $Config, + + [Sqlcollaborative.Dbatools.Configuration.ConfigScope] + $Scope, + + [bool] + $EnableException, + + [string] + $FunctionName = (Get-PSCallStack)[0].Command + ) + + if (-not $Config -or ($Config.RegistryData -eq "")) { + Stop-Function -Message "Invalid Input, cannot export $($Config.FullName), type not supported" -EnableException $EnableException -Category InvalidArgument -Target $Config -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "config", "fail" + return + } + + try { + Write-Message -Level Verbose -Message "Registering $($Config.FullName) for $Scope" -Target $Config -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "Config" + #region User Default + if (1 -band $Scope) { + Ensure-RegistryPath -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -ErrorAction Stop + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion User Default + + #region User Mandatory + if (2 -band $Scope) { + Ensure-RegistryPath -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -ErrorAction Stop + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion User Mandatory + + #region System Default + if (4 -band $Scope) { + Ensure-RegistryPath -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -ErrorAction Stop + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion System Default + + #region System Mandatory + if (8 -band $Scope) { + Ensure-RegistryPath -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -ErrorAction Stop + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion System Mandatory + } + catch { + Stop-Function -Message "Failed to export $($Config.FullName), to scope $Scope" -EnableException $EnableException -Target $Config -ErrorRecord $_ -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "config", "fail" + return + } + } + + function Ensure-RegistryPath { + [CmdletBinding()] + Param ( + [string] + $Path + ) + + if (-not (Test-Path $Path)) { + $null = New-Item $Path -Force -ErrorAction Stop + } + } + } + process { + switch ($parSet) { + "Default" { + foreach ($item in $Config) { + Write-Config -Config $item -Scope $Scope -EnableException $EnableException + } + + foreach ($item in $FullName) { + if ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.ContainsKey($item.ToLower())) { + Write-Config -Config ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$item.ToLower()]) -Scope $Scope -EnableException $EnableException + } + } + } + "Name" { + foreach ($item in ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name)) { + Write-Config -Config $item -Scope $Scope -EnableException $EnableException + } + } + } + } + end { + + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentJob { + <# + .SYNOPSIS + Remove-DbaAgentJob removes a job. + + .DESCRIPTION + Remove-DbaAgentJob removes a a job in the SQL Server Agent. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job. Can be null if the the job id is being used. + + .PARAMETER KeepHistory + Specifies to keep the history for the job. By default history is deleted. + + .PARAMETER KeepUnusedSchedule + Specifies to keep the schedules attached to this job if they are not attached to any other job. + By default the unused schedule is deleted. + + .PARAMETER Mode + Default: Strict + How strict does the command take lesser issues? + Strict: Interrupt if the job specified doesn't exist. + Lazy: Silently skip over jobs that don't exist. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Agent, Job + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaAgentJob + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 + + Removes the job from the instance with the name Job1 + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 -KeepHistory + + Removes the job but keeps the history + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 -KeepUnusedSchedule + + Removes the job but keeps the unused schedules + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job Job1 + + Removes the job from multiple servers + + .EXAMPLE + sql1, sql2, sql3 | Remove-DbaAgentJob -Job Job1 + + Removes the job from multiple servers using pipe line + + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $false)] + [switch]$KeepHistory, + [Parameter(Mandatory = $false)] + [switch]$KeepUnusedSchedule, + [DbaMode]$Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + Write-Message -Level Verbose -Message "Processing job $j" + + if ($Server.JobServer.Jobs.Name -notcontains $j) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Job $j doesn't exists on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Job $j doesn't exist on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + if ($PSCmdlet.ShouldProcess($instance, "Removing the job $j")) { + try { + $currentJob = $Server.JobServer.Jobs[$j] + $dropHistory = 1 + $dropSchedule = 1 + if (Test-Bound -ParameterName KeepHistory) { + Write-Message -Level SomewhatVerbose -Message "Job history will be kept" + $dropHistory = 0 + } + if (Test-Bound -ParameterName KeepUnusedSchedule) { + Write-Message -Level SomewhatVerbose -Message "Unused job schedules will be kept" + $dropSchedule = 0 + } + Write-Message -Level SomewhatVerbose -Message "Removing job" + $dropJobQuery = ("EXEC dbo.sp_delete_job @job_name = '{0}', @delete_history = {1}, @delete_unused_schedule = {2}" -f $currentJob.Name, $dropHistory, $dropSchedule) + $server.Databases['msdb'].ExecuteNonQuery($dropJobQuery) + } + catch { + Stop-Function -Message "Something went wrong removing the job" -Target $instance -ErrorRecord $_ -Continue + } + } + } + } + } + } + end { + Write-Message -Message "Finished removing jobs(s)." -Level Verbose + } +} +function Remove-DbaAgentJobCategory { + + <# +.SYNOPSIS +Remove-DbaAgentJobCategory removes a job category. + +.DESCRIPTION +Remove-DbaAgentJobCategory makes it possible to remove a job category. +Be assured that the category you want to remove is not used with other jobs. If another job uses this category it will be get the category [Uncategorized (Local)]. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaAgentJobCategory + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' + +Remove the job category Category 1 from the instance. + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1 -Category Category1, Category2, Category3 + +Remove multiple job categories from the instance. + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1, sql2, sql3 -Category Category1, Category2, Category3 + +Remove multiple job categories from the multiple instances. + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Loop through each of the categories + foreach ($cat in $Category) { + + # Check if the job category exists + if ($cat -notin $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat doesn't exist on $instance" -Target $instance -Continue + } + + # Remove the category + if ($PSCmdlet.ShouldProcess($instance, "Changing the job category $Category")) { + try { + # Get the category + $currentCategory = $server.JobServer.JobCategories[$cat] + + Write-Message -Message "Removing job category $cat" -Level Verbose + + $currentCategory.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } #if should process + + } # for each category + + } # for each instance + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished removing job category." -Level Verbose + } + +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentJobStep { + <# + .SYNOPSIS + Removes a step from the specified SQL Agent job. + + .DESCRIPTION + Removes a job step from a SQL Server Agent job. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job. + + .PARAMETER StepName + The name of the job step. + + .PARAMETER Mode + Default: Strict + How strict does the command take lesser issues? + Strict: Interrupt if the configuration already has the same value as the one specified. + Lazy: Silently skip over instances that already have this configuration at the specified value. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Agent, Job, JobStep + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaAgentJobStep + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 + + Remove 'Step1' from job 'Job1' on sql1. + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1 -Job Job1, Job2, Job3 -StepName Step1 + + Remove the job step from multiple jobs. + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 + + Remove the job step from the job on multiple servers. + + .EXAMPLE + sql1, sql2, sql3 | Remove-DbaAgentJobStep -Job Job1 -StepName Step1 + + Remove the job step from the job on multiple servers using pipeline. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [DbaMode]$Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + Write-Message -Level Verbose -Message "Processing job $j" + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Job $j doesn't exists on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Job $j doesnn't exist on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + # Check if the job step exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Step $StepName doesn't exist for $job on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Step $StepName doesn't exist for $job on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Removing the job step $StepName for job $j")) { + try { + $JobStep = $Server.JobServer.Jobs[$j].JobSteps[$StepName] + Write-Message -Level SomewhatVerbose -Message "Removing the job step $StepName for job $j." + $JobStep.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job step" -Target $JobStep -Continue -ErrorRecord $_ + Write-Message -Level Verbose -Message "Could not remove the job step $StepName from $j" + } + } + } + } + } + } + } + end { + Write-Message -Message "Finished removing the jobs step(s)" -Level Verbose + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentSchedule { + <# +.SYNOPSIS +Remove-DbaAgentJobSchedule removes a job schedule. + +.DESCRIPTION +Remove-DbaAgentJobSchedule removes a a job in the SQL Server Agent. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Schedule +The name of the job schedule. + +.PARAMETER InputObject +A collection of schedule (such as returned by Get-DbaAgentSchedule), to be removed. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. +It will also remove the any present schedules with the same name for the specific job. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, Schedule + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaAgentJobSchedule + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule weekly +Remove the schedule weekly + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule weekly -Force +Remove the schedule weekly from the job even if the schedule is being used by another job. + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule daily, weekly +Remove multiple schedule + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1, sql2, sql3 -Schedule daily, weekly +Remove the schedule on multiple servers for multiple schedules + +.EXAMPLE +sql1, sql2, sql3 | Remove-DbaAgentSchedule -Schedule daily, weekly +Remove the schedule on multiple servers using pipe line + +.EXAMPLE +Get-DbaAgentSchedule -SqlInstance sql1 -Schedule sched1, sched2, sched3 | Remove-DbaAgentSchedule + +Remove the schedules using a pipeline + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential] + $SqlCredential, + [Parameter(Mandatory = $true, ParameterSetName = "instance")] + [ValidateNotNullOrEmpty()] + [Alias("Schedules")] + [object[]]$Schedule, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "schedules")] + [Microsoft.SqlServer.Management.Smo.Agent.ScheduleBase[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException, + [switch]$Force + ) + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $InputObject += $server.JobServer.SharedSchedules | Where-Object { $_.Name -in $Schedule } + + } # foreach object instance + + foreach ($s in $InputObject) { + + if ($Server.JobServer.SharedSchedules.Name -contains $s.Name) { + # Get job count + $jobCount = $Server.JobServer.SharedSchedules[$s].JobCount + + # Check if the schedule is shared among other jobs + if ($jobCount -ge 1 -and -not $Force) { + Stop-Function -Message "The schedule $s is shared connected to one or more jobs. If removal is neccesary use -Force." -Target $instance -Continue + } + + # Remove the job schedule + if ($PSCmdlet.ShouldProcess($instance, "Removing schedule $s on $instance")) { + # Loop through each of the schedules and drop them + Write-Message -Message "Removing schedule $s on $instance" -Level Verbose + + #Check if jobs use the schedule + if ($jobCount -ge 1) { + # Get the job object + $smoSchedules = $server.JobServer.SharedSchedules | Where-Object {($_.Name -eq $s.Name)} + + Write-Message -Message "Schedule $sched is used in one or more jobs. Removing it for each job." -Level Verbose + + # Loop through each if the schedules + foreach ($smoSchedule in $smoSchedules) { + + # Get the job ids + $jobGuids = $Server.JobServer.SharedSchedules[$smoSchedule].EnumJobReferences() + + if (($jobCount -gt 1 -and $Force) -or $jobCount -eq 1) { + + # Loop though each of the jobs + foreach ($guid in $jobGuids) { + # Get the job object + $smoJob = $Server.JobServer.GetJobByID($guid) + + # Get the job schedule + $jobSchedules = $Server.JobServer.Jobs[$smoJob].JobSchedules | Where-Object {$_.Name -eq $smoSchedule} + + foreach ($jobSchedule in $jobSchedules) { + try { + Write-Message -Message "Removing the schedule $jobSchedule for job $smoJob" -Level Verbose + + $jobSchedule.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job schedule" -Target $instance -ErrorRecord $_ -Continue + } + } + } # foreach guid + } # if jobcount + + } # foreach smoschedule + } # if jobcount ge 1 + + Write-Message -Message "Removing schedules that are not being used by other jobs." -Level Verbose + + # Get the schedules + $smoSchedules = $server.JobServer.SharedSchedules | Where-Object {($_.Name -eq $s.Name) -and ($_.JobCount -eq 0)} + + # Remove the schedules that have no jobs + foreach ($smoSchedule in $smoSchedules) { + try { + $smoSchedule.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the schedule" -Target $instance -ErrorRecord $_ -Continue + } + } # foreach schedule + } # should process + } # if contains schedule + else { + Stop-Function -Message "Schedule $s is not present on instance $instance" -Target $instance -Continue + } + } #foreach object schedule + + } # process + + end { + Write-Message -Message "Finished removing jobs schedule(s)." -Level Verbose + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaBackup { + <# + .SYNOPSIS + Removes SQL Server backups from disk. + + .DESCRIPTION + Provides all of the same functionality for removing SQL backups from disk as a standard maintenance plan would. + + As an addition you have the ability to check the Archive bit on files before deletion. This will allow you to ensure backups have been archived to your archive location before removal. + + Also included is the ability to remove empty folders as part of this cleanup activity. + + .PARAMETER Path + Specifies the name of the base level folder to search for backup files. Deletion of backup files will be recursive from this location. + + .PARAMETER BackupFileExtension + Specifies the filename extension of the backup files you wish to remove (typically 'bak', 'trn' or 'log'). Do not include the period. + + .PARAMETER RetentionPeriod + Specifies the retention period for backup files. Correct format is ##U. + + ## is the retention value and must be an integer value + U signifies the units where the valid units are: + h = hours + d = days + w = weeks + m = months + + Formatting Examples: + '48h' = 48 hours + '7d' = 7 days + '4w' = 4 weeks + '1m' = 1 month + + .PARAMETER CheckArchiveBit + If this switch is enabled, the filesystem Archive bit is checked before deletion. If this bit is set (which translates to "it has not been backed up to another location yet", the file won't be deleted. + + .PARAMETER RemoveEmptyBackupFolder + If this switch is enabled, empty folders will be removed after the cleanup process is complete. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.i + + .NOTES + Tags: Storage, DisasterRecovery, Backup + Author: Chris Sommer, @cjsommer, www.cjsommer.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaBackup + + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h + + '*.trn' files in 'C:\MSSQL\SQL Backup\' and all subdirectories that are more than 48 hours old will be removed. + + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h -WhatIf + + Same as example #1, but doesn't actually remove any files. The function will instead show you what would be done. + This is useful when first experimenting with using the function. + + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 7d -CheckArchiveBit + + '*.bak' files in 'C:\MSSQL\Backup\' and all subdirectories that are more than 7 days old will be removed, but only if the files have been backed up to another location as verified by checking the Archive bit. + + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 1w -RemoveEmptyBackupFolder + + '*.bak' files in 'C:\MSSQL\Backup\' and all subdirectories that are more than 1 week old will be removed. Any folders left empty will be removed as well. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, HelpMessage = "Full path to the root level backup folder (ex. 'C:\SQL\Backups'")] + [Alias("BackupFolder")] + [string]$Path, + [parameter(Mandatory = $true, HelpMessage = "Backup File extension to remove (ex. bak, trn, dif)")] + [string]$BackupFileExtension , + [parameter(Mandatory = $true, HelpMessage = "Backup retention period. (ex. 24h, 7d, 4w, 6m)")] + [string]$RetentionPeriod , + [parameter(Mandatory = $false)] + [switch]$CheckArchiveBit = $false , + [parameter(Mandatory = $false)] + [switch]$RemoveEmptyBackupFolder = $false, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + # Ensure BackupFileExtension does not begin with a . + if ($BackupFileExtension -match "^[.]") { + Write-Message -Level Warning -Message "Parameter -BackupFileExtension begins with a period '$BackupFileExtension'. A period is automatically prepended to -BackupFileExtension and need not be passed in." + } + } + process { + # Process stuff + Write-Message -Message "Removing backups from $Path" -Level Verbose + Find-DbaBackup -Path $Path -BackupFileExtension $BackupFileExtension -RetentionPeriod $RetentionPeriod -CheckArchiveBit:$CheckArchiveBit -EnableException | + Foreach-Object { + $file = $_ + if ($PSCmdlet.ShouldProcess($file.Directory.FullName, "Removing backup file $($file.Name)")) { + try { + $file | Remove-Item -Force -EA Stop + } + catch { + Write-Message -Message "Failed to remove $file." -Level Warning -ErrorRecord $_ + } + } + } + Write-Message -Message "File Cleaning ended." -Level Verbose + # Cleanup empty backup folders. + if ($RemoveEmptyBackupFolder) { + Write-Message -Message "Removing empty folders." -Level Verbose + (Get-ChildItem -Directory -Path $Path -Recurse -ErrorAction SilentlyContinue -ErrorVariable EnumErrors).FullName | + Sort-Object -Descending | + Foreach-Object { + $OrigPath = $_ + try { + $Contents = @(Get-ChildItem -Force $OrigPath -ErrorAction Stop) + } + catch { + Write-Message -Message "Can't enumerate $OrigPath." -Level Warning -ErrorRecord $_ + } + if ($Contents.Count -eq 0) { + return $_ + } + } | + Foreach-Object { + $FolderPath = $_ + if ($PSCmdlet.ShouldProcess($Path, "Removing empty folder .$($FolderPath.Replace($Path, ''))")) { + try { + $FolderPath | Remove-Item -ErrorAction Stop + } + catch { + Write-Message -Message "Failed to remove $FolderPath." -Level Warning -ErrorRecord $_ + } + } + } + if ($EnumErrors) { + Write-Message "Errors encountered enumerating folders." -Level Warning -ErrorRecord $EnumErrors + } + Write-Message -Message "Removed empty folders." -Level Verbose -EnableException $EnableException + } + } +} +function Remove-DbaClientAlias { + <# + .SYNOPSIS + Removes a sql alias for the specified server - mimics cliconfg.exe + + .DESCRIPTION + Removes a SQL Server alias by altering HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client + + .PARAMETER ComputerName + The target computer where the alias will be created + + .PARAMETER Credential + Allows you to login to remote computers using alternative credentials + + .PARAMETER Alias + The alias to be deleted + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Alias + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaClientAlias + + .EXAMPLE + Remove-DbaClientAlias -ComputerName workstationx -Alias sqlps + Removes the sqlps SQL client alias on workstationx + + .EXAMPLE + Get-DbaClientAlias | Remove-DbaClientAlias + Removes all SQL Server client aliases on the local computer + +#> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipelineByPropertyName)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('AliasName')] + [string]$Alias, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($computer in $ComputerName) { + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + $scriptblock = { + $Alias = $args[0] + function Get-ItemPropertyValue { + Param ( + [parameter()] + [String]$Path, + [parameter()] + [String]$Name + ) + Get-ItemProperty -LiteralPath $Path -Name $Name + } + + $basekeys = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer", "HKLM:\SOFTWARE\Microsoft\MSSQLServer" + + foreach ($basekey in $basekeys) { + + if ((Test-Path $basekey) -eq $false) { + Write-Warning "Base key ($basekey) does not exist. Quitting." + continue + } + + $client = "$basekey\Client" + + if ((Test-Path $client) -eq $false) { + continue + } + + $connect = "$client\ConnectTo" + + if ((Test-Path $connect) -eq $false) { + continue + } + + if ($basekey -like "*WOW64*") { + $architecture = "32-bit" + } + else { + $architecture = "64-bit" + } + + + $all = Get-Item -Path $connect + foreach ($entry in $all) { + + foreach ($en in $entry) { + $e = $entry.ToString().Replace('HKEY_LOCAL_MACHINE', 'HKLM:\') + if ($en.Property -contains $Alias) { + Remove-ItemProperty -Path $e -Name $Alias + } + else { + $en + } + } + } + } + } + + if ($PScmdlet.ShouldProcess($computer, "Getting aliases")) { + try { + $null = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ErrorAction Stop -Verbose:$false -ArgumentList $Alias + Get-DbaClientAlias -ComputerName $computer + + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} +function Remove-DbaCmConnection { + <# + .SYNOPSIS + Removes connection objects from the connection cache used for remote computer management. + + .DESCRIPTION + Removes connection objects from the connection cache used for remote computer management. + + .PARAMETER ComputerName + The computer whose connection to remove. + Accepts both text as well as the output of Get-DbaCmConnection. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaCmConnection + + .EXAMPLE + Remove-DbaCmConnection -ComputerName sql2014 + + Removes the cached connection to the server sql2014 from the cache. + + .EXAMPLE + Get-DbaCmConnection | Remove-DbaCmConnection + + Clears the entire connection cache. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true, Mandatory = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + } + PROCESS { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Removing from connection cache: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + if ([Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections.ContainsKey($connectionObject.Connection.ComputerName)) { + $null = [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections.Remove($connectionObject.Connection.ComputerName) + Write-Message -Level Verbose -Message "Successfully removed $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + } + else { + Write-Message -Level Verbose -Message "Not found: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + } + } + } + END { + Write-Message -Level InternalComment -Message "Ending" + } +} +function Remove-DbaComputerCertificate { + <# + .SYNOPSIS + Removes a computer certificate - useful for removing easily certs from remote computers + + .DESCRIPTION + Removes a computer certificate from a local or remote compuer + + .PARAMETER ComputerName + The target computer - defaults to localhost + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials + + .PARAMETER Thumbprint + The thumbprint of the certificate object + + .PARAMETER Store + Certificate store - defaults to LocalMachine (otherwise exceptions can be thrown on remote connections) + + .PARAMETER Folder + Certificate folder + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaComputerCertificate -ComputerName Server1 -Thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 + + Removes certificate with thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 in the LocalMachine store on Server1 + + .EXAMPLE + Get-DbaComputerCertificate | Where-Object Thumbprint -eq E0A071E387396723C45E92D42B2D497C6A182340 | Remove-DbaComputerCertificate + + Removes certificate using the pipeline + + .EXAMPLE + Remove-DbaComputerCertificate -ComputerName Server1 -Thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 -Store User -Folder My + + Removes certificate with thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 in the User\My (Personal) store on Server1 + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(ValueFromPipelineByPropertyName, Mandatory)] + [string[]]$Thumbprint, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + #region Scriptblock for remoting + $scriptblock = { + param ( + $Thumbprint, + + $Store, + + $Folder + ) + Write-Verbose "Searching Cert:\$Store\$Folder for thumbprint: $thumbprint" + $cert = Get-ChildItem "Cert:\$store\$folder" -Recurse | Where-Object { $_.Thumbprint -eq $Thumbprint } + + if ($cert) { + $null = $cert | Remove-Item + $status = "Removed" + } + else { + $status = "Certificate not found in Cert:\$Store\$Folder" + } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Store = $Store + Folder = $Folder + Thumbprint = $thumbprint + Status = $status + } + } + #endregion Scriptblock for remoting + } + + process { + foreach ($computer in $computername) { + foreach ($thumb in $Thumbprint) { + if ($PScmdlet.ShouldProcess("local", "Connecting to $computer to remove cert from Cert:\$Store\$Folder")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ArgumentList $thumb, $Store, $Folder -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message $_ -ErrorRecord $_ -Target $computer -Continue + } + } + } + } + } +} +function Remove-DbaDatabase { + <# +.SYNOPSIS +Drops a database, hopefully even the really stuck ones. + +.DESCRIPTION +Tries a bunch of different ways to remove a database or two or more. + +.PARAMETER SqlInstance +The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER InputObject +A collection of databases (such as returned by Get-DbaDatabase), to be removed. + +.PARAMETER IncludeSystemDb +Use this switch to disable any kind of verbose messages + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Delete, Databases + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaDatabase + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb + +Prompts then removes the database containeddb on SQL Server sql2016 + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb, mydb + +Prompts then removes the databases containeddb and mydb on SQL Server sql2016 + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb -Confirm:$false + +Does not prompt and swiftly removes containeddb on SQL Server sql2016 + +.EXAMPLE +Get-DbaDatabase -SqlInstance server\instance -ExcludeAllSystemDb | Remove-DbaDatabase + +Removes all the user databases from server\instance + +.EXAMPLE +Get-DbaDatabase -SqlInstance server\instance -ExcludeAllSystemDb | Remove-DbaDatabase -Confirm:$false + +Removes all the user databases from server\instance without any confirmation +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = "Default")] + Param ( + [parameter(, Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(Mandatory = $false)] + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("Databases")] + [object[]]$Database, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "databases")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [switch]$IncludeSystemDb, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Databases | Where-Object { $_.Name -in $Database } + } + + $system_dbs = @( "master", "model", "tempdb", "resource", "msdb" ) + + if (-not($IncludeSystemDb)) { + $InputObject = $InputObject | Where-Object { $_.Name -notin $system_dbs} + } + + foreach ($db in $InputObject) { + try { + $server = $db.Parent + if ($Pscmdlet.ShouldProcess("$db on $server", "KillDatabase")) { + $server.KillDatabase($db.name) + $server.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + try { + if ($Pscmdlet.ShouldProcess("$db on $server", "alter db set single_user with rollback immediate then drop")) { + $null = $server.Query("if exists (select * from sys.databases where name = '$($db.name)' and state = 0) alter database $db set single_user with rollback immediate; drop database $db") + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + try { + if ($Pscmdlet.ShouldProcess("$db on $server", "SMO drop")) { + $server.databases[$dbname].Drop() + $server.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + Write-Message -Level Verbose -Message "Could not drop database $db on $server" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = $_ + } + } + } + } + } + } +} +function Remove-DbaDatabaseMasterKey { + <# + .SYNOPSIS + Deletes specified database master key + + .DESCRIPTION + Deletes specified database master key. + + .PARAMETER SqlInstance + The target SQL Server instance. + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials. + + .PARAMETER Database + The database where the master key will be removed. + + .PARAMETER ExcludeDatabase + List of databases to exclude from clearing all master keys + + .PARAMETER All + Purge the master keys from all databases on an instance. + + .PARAMETER MasterKeyCollection + Internal parameter to support pipeline input + + .PARAMETER Mode + Controls how the function handles cases where it can't do anything due to missing database or key: + Strict: Write a warning (default) + Lazy: Write a verbose message + Report: Create a report object as part of the output + The default action can be adjusted by using Set-DbaConfig to change the 'message.mode.default' configuration + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaDatabaseMasterKey -SqlInstance Server1 + + The master key in the master database on server1 will be removed if it exists. + + .EXAMPLE + Remove-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 -Confirm:$false + + Suppresses all prompts to remove the master key in the 'db1' database and drops the key. + + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ParameterSetName = "instanceExplicit")] + [parameter(Mandatory, ParameterSetName = "instanceAll")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance, + + [System.Management.Automation.PSCredential] + $SqlCredential, + + [parameter(Mandatory, ParameterSetName = "instanceExplicit")] + [object[]] + $Database, + + [parameter(ParameterSetName = "instanceAll")] + [object[]] + $ExcludeDatabase, + + [parameter(Mandatory, ParameterSetName = "instanceAll")] + [switch] + $All, + + [parameter(ValueFromPipeline, ParameterSetName = "collection")] + [Microsoft.SqlServer.Management.Smo.MasterKey[]] + $MasterKeyCollection, + + [DbaMode] + $Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + function Drop-Masterkey { + [CmdletBinding()] + Param ( + $masterkey, + + $mode = $Mode, + + $EnableException = $EnableException + ) + $server = $masterkey.Parent.Parent + $instance = $server.DomainInstanceName + $cert = $masterkey.Name + $db = $masterkey.Parent + + if ($Pscmdlet.ShouldProcess($instance, "Dropping the master key for database '$db'")) { + try { + $masterkey.Drop() + Write-Message -Level Verbose -Message "Successfully removed master key from the $db database on $instance" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Status = "Success" + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Status = "Failure" + } + Stop-Function -Message "Failed to drop master key from $db on $instance." -Target $db -InnerErrorRecord $_ -Continue + } + } + } + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($All) { + $Database = ($server.Databases | Where-Object Name -NotIn $ExcludeDatabase).Name + } + + :Database foreach ($db in $Database) { + $smodb = $server.Databases[$db] + $masterkey = $smodb.MasterKey + + #region Case: Database Unknown + if ($null -eq $smodb) { + switch ($Mode) { + [DbaMode]::Strict { Stop-Function -Message "Database '$db' does not exist on $instance" -Target $smodb -Continue -ContinueLabel database } + [DbaMode]::Lazy { + Write-Message -Level (Get-DbaConfigValue -Name 'message.mode.lazymessagelevel' -Fallback 4) -Message "Database '$db' does not exist on $instance" -Target $smodb + continue database + } + [DbaMode]::Report { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + Status = "Unknown Database" + } + continue Database + } + } + } + #endregion Case: Database Unknown + + #region Case: No Master Key + if ($null -eq $masterkey) { + switch ($Mode.ToString()) { + "Strict" { Stop-Function -Message "No master key exists in the $db database on $instance" -Target $smodb -Continue -ContinueLabel database } + "Lazy" { + Write-Message -Level (Get-DbaConfigValue -Name 'message.mode.lazymessagelevel' -Fallback 4) -Message "No master key exists in the $db database on $instance" -Target $smodb + continue database + } + "Report" { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $smodb.Name + Status = "No Masterkey" + } + continue Database + } + } + } + #endregion Case: No Master Key + + Write-Message -Level Verbose -Message "Removing master key from $db" + Drop-Masterkey -masterkey $masterkey + } + } + + foreach ($key in $MasterKeyCollection) { + Write-Message -Level Verbose -Message "Removing master key: $key" + Drop-Masterkey -masterkey $key + } + } +} +function Remove-DbaDatabaseSafely { + <# + .SYNOPSIS + Safely removes a SQL Database and creates an Agent Job to restore it. + + .DESCRIPTION + Performs a DBCC CHECKDB on the database, backs up the database with Checksum and verify only to a final (golden) backup location, creates an Agent Job to restore from that backup, drops the database, runs the agent job to restore the database, performs a DBCC CHECKDB and drops the database. + + With huge thanks to Grant Fritchey and his verify your backups video. Take a look, it's only 3 minutes long. http://sqlps.io/backuprant + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + If specified, Agent jobs will be created on this server. By default, the jobs will be created on the server specified by SqlInstance. You must have sysadmin access and the server must be SQL Server 2000 or higher. The SQL Agent service will be started if it is not already running. + + .PARAMETER DestinationCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more databases to remove. + + .PARAMETER NoDbccCheckDb + If this switch is enabled, the initial DBCC CHECK DB will be skipped. This will make the process quicker but will also allow you to create an Agent job that restores a database backup containing a corrupt database. + + A second DBCC CHECKDB is performed on the restored database so you will still be notified BUT USE THIS WITH CARE. + + .PARAMETER BackupFolder + Specifies the path to a folder where the final backups of the removed databases will be stored. If you are using separate source and destination servers, you must specify a UNC path such as \\SERVER1\BACKUPSHARE\ + + .PARAMETER JobOwner + Specifies the name of the account which will own the Agent jobs. By default, sa is used. + + .PARAMETER UseDefaultFilePaths + If this switch is enabled, the default file paths for the data and log files on the instance where the database is restored will be used. By default, the original file paths will be used. + + .PARAMETER CategoryName + Specifies the Category Name for the Agent job that is created for restoring the database(s). By default, the name is "Rationalisation". + + .PARAMETER BackupCompression + If this switch is enabled, compression will be used for the backup regardless of the SQL Server instance setting. By default, the SQL Server instance setting for backup compression is used. + + .PARAMETER AllDatabases + If this switch is enabled, all user databases on the server will be removed. This is useful when decommissioning a server. You should use a DestinationServer with this switch. + + .PARAMETER ReuseSourceFolderStructure + If this switch is enabled, the source folder structure will be used when restoring instead of using the destination instance default folder structure. + + .PARAMETER Force + If this switch is enabled, all actions will be performed even if DBCC errors are detected. An Agent job will be created with 'DBCCERROR' in the name and the backup file will have 'DBCC' in its name. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Remove + Author: Rob Sewell @SQLDBAWithBeard, sqldbawithabeard.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDatabaseSafely + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -Database RideTheLightning -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on database RideTheLightning on server Fade2Black. If there are no errors, the database is backup to the folder C:\MSSQL\Backup\Rationalised - DO NOT DELETE. Then, an Agent job to restore the database from that backup is created. The database is then dropped, the Agent job to restore it run, a DBCC CHECKDB run against the restored database, and then it is dropped again. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + $Database = 'DemoNCIndex','RemoveTestDatabase' + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -Database $Database -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on two databases, 'DemoNCIndex' and 'RemoveTestDatabase' on server Fade2Black. Then, an Agent job to restore each database from those backups is created. The databases are then dropped, the Agent jobs to restore them run, a DBCC CHECKDB run against the restored databases, and then they are dropped again. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -DestinationServer JusticeForAll -Database RideTheLightning -BackupFolder '\\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on database RideTheLightning on server Fade2Black. If there are no errors, the database is backup to the folder \\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE . Then, an Agent job is created on server JusticeForAll to restore the database from that backup is created. The database is then dropped on Fade2Black, the Agent job to restore it on JusticeForAll is run, a DBCC CHECKDB run against the restored database, and then it is dropped from JusticeForAll. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance IronMaiden -Database $Database -DestinationServer TheWildHearts -BackupFolder Z:\Backups -NoDbccCheckDb -UseDefaultFilePaths -JobOwner 'THEBEARD\Rob' + + For the databases $Database on the server IronMaiden a DBCC CHECKDB will not be performed before backing up the databases to the folder Z:\Backups. Then, an Agent job is created on server TheWildHearts with a Job Owner of THEBEARD\Rob to restore each database from that backup using the instance's default file paths. The database(s) is(are) then dropped on IronMaiden, the Agent job(s) run, a DBCC CHECKDB run on the restored database(s), and then the database(s) is(are) dropped. + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance IronMaiden -Database $Database -DestinationServer TheWildHearts -BackupFolder Z:\Backups -UseDefaultFilePaths -ContinueAfterDbccError + + The databases $Database on the server IronMaiden will be backed up the to the folder Z:\Backups. Then, an Agent job is created on server TheWildHearts with a Job Owner of THEBEARD\Rob to restore each database from that backup using the instance's default file paths. The database(s) is(are) then dropped on IronMaiden, the Agent job(s) run, a DBCC CHECKDB run on the restored database(s), and then the database(s) is(are) dropped. + + If there is a DBCC Error, the function will continue to perform rest of the actions and will create an Agent job with 'DBCCERROR' in the name and a Backup file with 'DBCCError' in the name. + #> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [parameter(Mandatory = $false)] + [DbaInstanceParameter]$Destination = $sqlinstance, + [PSCredential] + $DestinationCredential, + [parameter(Mandatory = $false)] + [Alias("NoCheck")] + [switch]$NoDbccCheckDb, + [parameter(Mandatory = $true)] + [string]$BackupFolder, + [parameter(Mandatory = $false)] + [string]$CategoryName = 'Rationalisation', + [parameter(Mandatory = $false)] + [string]$JobOwner, + [parameter(Mandatory = $false)] + [switch]$AllDatabases, + [ValidateSet("Default", "On", "Of")] + [string]$BackupCompression = 'Default', + [switch]$ReuseSourceFolderStructure, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (!$AllDatabases -and !$Database) { + Stop-Function -Message "You must specify at least one database. Use -Database or -AllDatabases." -ErrorRecord $_ + return + } + + $sourceserver = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlCredential -ParameterConnection + + if (-not $destination) { + $destination = $sqlinstance + $DestinationCredential = $SqlCredential + } + + if ($sqlinstance -ne $destination) { + + $destserver = Connect-SqlInstance -SqlInstance $destination -SqlCredential $DestinationCredential + + $sourcenb = $instance.ComputerName + $destnb = $instance.ComputerName + + if ($BackupFolder.StartsWith("\\") -eq $false -and $sourcenb -ne $destnb) { + Stop-Function -Message "Backup folder must be a network share if the source and destination servers are not the same." -ErrorRecord $_ -Target $backupFolder + return + } + } + else { + $destserver = $sourceserver + } + + $source = $sourceserver.DomainInstanceName + $destination = $destserver.DomainInstanceName + + if (!$jobowner) { + $jobowner = Get-SqlSaLogin $destserver + } + + if ($alldatabases -or !$Database) { + $database = ($sourceserver.databases | Where-Object { $_.IsSystemObject -eq $false -and ($_.Status -match 'Offline') -eq $false }).Name + } + + if (!(Test-DbaSqlPath -SqlInstance $destserver -Path $backupFolder)) { + $serviceaccount = $destserver.ServiceAccount + Stop-Function -Message "Can't access $backupFolder Please check if $serviceaccount has permissions." -ErrorRecord $_ -Target $backupFolder + } + + $jobname = "Rationalised Final Database Restore for $dbname" + $jobStepName = "Restore the $dbname database from Final Backup" + + if (!($destserver.Logins | Where-Object { $_.Name -eq $jobowner })) { + Stop-Function -Message "$destination does not contain the login $jobowner - Please fix and try again - Aborting." -ErrorRecord $_ -Target $jobowner + } + + function Start-SqlAgent { + <# + .SYNOPSIS + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param () + if ($destserver.VersionMajor -eq 8) { + $serviceName = 'MSSQLSERVER' + } + else { + $instance = $destserver.InstanceName + if ($instance.length -eq 0) { $instance = "MSSQLSERVER" } + $serviceName = "SQL Server Agent ($instance)" + } + + if ($Pscmdlet.ShouldProcess($destination, "Starting Sql Agent")) { + try { + $ipaddr = Resolve-SqlIpAddress $destserver + $agentservice = Get-Service -ComputerName $ipaddr -DisplayName $serviceName + + if ($agentservice.Status -ne 'Running') { + $agentservice.Start() + $timeout = New-Timespan -seconds 60 + $sw = [diagnostics.stopwatch]::StartNew() + $agentstatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status + while ($AgentStatus -ne 'Running' -and $sw.elapsed -lt $timeout) { + $agentStatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status + } + } + } + + catch { + throw $_ + } + + if ($agentservice.Status -ne 'Running') { + throw "Cannot start Agent Service on $destination - Aborting." + } + } + } + + function Start-DbccCheck { + <# + .SYNOPSIS + + #> + + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [object]$server, + [string]$dbname + ) + + $servername = $server.name + $db = $server.databases[$dbname] + + if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $servername")) { + try { + $null = $db.CheckTables('None') + Write-Message -Level Verbose -Message "DBCC CHECKDB finished successfully for $dbname on $servername." + } + + catch { + Write-Message -Level Warning -Message "DBCC CHECKDB failed." + Stop-Function -Message "Error occured: $_" -Target $agentservice -ErrorRecord $_ -Continue + + if ($force) { + return $true + } + else { + return $false + } + } + } + } + + function New-SqlAgentJobCategory { + <# + .SYNOPSIS + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ([string]$categoryname, + [object]$jobServer) + + if (!$jobServer.JobCategories[$categoryname]) { + if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $sourceserver")) { + try { + Write-Message -Level Verbose -Message "Creating Agent Job Category $categoryname." + $category = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobCategory + $category.Parent = $jobServer + $category.Name = $categoryname + $category.Create() + Write-Message -Level Verbose -Message "Created Agent Job Category $categoryname." + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job Category - $categoryname - Aborting." -Target $categoryname -ErrorRecord $_ + return + } + } + } + } + + function Restore-Database { + <# + .SYNOPSIS + Internal function. Restores .bak file to Sql database. Creates db if it doesn't exist. $filestructure is + a custom object that contains logical and physical file locations. + #> + + param ( + [Parameter(Mandatory = $true)] + [Alias('ServerInstance', 'SqlInstance', 'SqlServer')] + [object]$server, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$dbname, + [Parameter(Mandatory = $true)] + [string]$backupfile, + [string]$filetype = 'Database', + [Parameter(Mandatory = $true)] + [object]$filestructure, + [switch]$norecovery, + [PSCredential]$sqlCredential, + [switch]$TSql = $false + ) + + $server = Connect-SqlInstance -SqlInstance $server -SqlCredential $sqlCredential + $servername = $server.name + $server.ConnectionContext.StatementTimeout = 0 + $restore = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' + $restore.ReplaceDatabase = $true + + foreach ($file in $filestructure.values) { + $movefile = New-Object 'Microsoft.SqlServer.Management.Smo.RelocateFile' + $movefile.LogicalFileName = $file.logical + $movefile.PhysicalFileName = $file.physical + $null = $restore.RelocateFiles.Add($movefile) + } + + try { + if ($TSql) { + $restore.PercentCompleteNotification = 1 + $restore.add_Complete($complete) + $restore.ReplaceDatabase = $true + $restore.Database = $dbname + $restore.Action = $filetype + $restore.NoRecovery = $norecovery + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $device.name = $backupfile + $device.devicetype = 'File' + $restore.Devices.Add($device) + $restorescript = $restore.script($server) + return $restorescript + } + else { + $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $restore.add_PercentComplete($percent) + $restore.PercentCompleteNotification = 1 + $restore.add_Complete($complete) + $restore.ReplaceDatabase = $true + $restore.Database = $dbname + $restore.Action = $filetype + $restore.NoRecovery = $norecovery + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $device.name = $backupfile + $device.devicetype = 'File' + $restore.Devices.Add($device) + + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $restore.sqlrestore($server) + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -status 'Complete' -Completed + + return $true + } + } + catch { + Stop-Function -Message "Restore failed" -ErrorRecord $_ -Target $dbname + return $false + } + } + + } + process { + if (Test-FunctionInterrupt) { + return + } + try { + Start-SqlAgent + } + catch { + Stop-Function -Message "Failure starting SQL Agent" -ErrorRecord $_ + return + } + + $start = Get-Date + Write-Message -Level Verbose -Message "Starting Rationalisation Script at $start." + + foreach ($dbname in $Database) { + + $db = $sourceserver.databases[$dbname] + + # The db check is needed when the number of databases exceeds 255, then it's no longer auto-populated + if (!$db) { + Stop-Function -Message "$dbname does not exist on $source. Aborting routine for this database." -Continue + } + + $lastFullBckDuration = (Get-DbaBackupHistory -SqlInstance $sourceserver -Database $dbname -LastFull).Duration + + if (-NOT ([string]::IsNullOrEmpty($lastFullBckDuration))) { + $lastFullBckDurationSec = $lastFullBckDuration.TotalSeconds + $lastFullBckDurationMin = [Math]::Round($lastFullBckDuration.TotalMinutes, 2) + + Write-Message -Level Verbose -Message "From the backup history the last full backup took $lastFullBckDurationSec seconds ($lastFullBckDurationMin minutes)" + if ($lastFullBckDurationSec -gt 600) { + Write-Message -Level Verbose -Message "Last full backup took more than 10 minutes. Do you want to continue?" + + # Set up the parts for the user choice + $Title = "Backup duration" + $Info = "Last full backup took more than $lastFullBckDurationMin minutes. Do you want to continue?" + + $Options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No (Skip)") + [int]$Defaultchoice = 0 + $choice = $host.UI.PromptForChoice($Title, $Info, $Options, $Defaultchoice) + # Check the given option + if ($choice -eq 1) { + Stop-Function -Message "You have chosen skipping the database $dbname because of last known backup time ($lastFullBckDurationMin minutes)." -ErrorRecord $_ -Target $dbname -Continue + Continue + } + } + } + else { + Write-Message -Level Verbose -Message "Couldn't find last full backup time for database $dbname using Get-DbaBackupHistory." + } + + $jobname = "Rationalised Database Restore Script for $dbname" + $jobStepName = "Restore the $dbname database from Final Backup" + $jobServer = $destserver.JobServer + + if ($jobServer.Jobs[$jobname].count -gt 0) { + if ($force -eq $false) { + Stop-Function -Message "FAILED: The Job $jobname already exists. Have you done this before? Rename the existing job and try again or use -Force to drop and recreate." -Continue + } + else { + if ($Pscmdlet.ShouldProcess($dbname, "Dropping $jobname on $source")) { + Write-Message -Level Verbose -Message "Dropping $jobname on $source." + $jobServer.Jobs[$jobname].Drop() + $jobServer.Jobs.Refresh() + } + } + } + + + Write-Message -Level Verbose -Message "Starting Rationalisation of $dbname." + ## if we want to Dbcc before to abort if we have a corrupt database to start with + if ($NoDbccCheckDb -eq $false) { + if ($Pscmdlet.ShouldProcess($dbname, "Running dbcc check on $dbname on $source")) { + Write-Message -Level Verbose -Message "Starting DBCC CHECKDB for $dbname on $source." + $dbccgood = Start-DbccCheck -Server $sourceserver -DBName $dbname + + if ($dbccgood -eq $false) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "DBCC failed for $dbname (you should check that). Aborting routine for this database." + continue + } + else { + Write-Message -Level Verbose -Message "DBCC failed, but Force specified. Continuing." + } + } + } + } + + if ($Pscmdlet.ShouldProcess($source, "Backing up $dbname")) { + Write-Message -Level Verbose -Message "Starting Backup for $dbname on $source." + ## Take a Backup + try { + $timenow = [DateTime]::Now.ToString('yyyyMMdd_HHmmss') + $backup = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Backup + $backup.Action = [Microsoft.SqlServer.Management.SMO.BackupActionType]::Database + $backup.BackupSetDescription = "Final Full Backup of $dbname Prior to Dropping" + $backup.Database = $dbname + $backup.Checksum = $True + if ($sourceserver.versionMajor -gt 9) { + $backup.CompressionOption = $BackupCompression + } + if ($force -and $dbccgood -eq $false) { + + $filename = "$backupFolder\$($dbname)_DBCCERROR_$timenow.bak" + } + else { + $filename = "$backupFolder\$($dbname)_Final_Before_Drop_$timenow.bak" + } + + $devicetype = [Microsoft.SqlServer.Management.Smo.DeviceType]::File + $backupDevice = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem($filename, $devicetype) + + $backup.Devices.Add($backupDevice) + #Progress + $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $backup.add_PercentComplete($percent) + $backup.add_Complete($complete) + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $backup.SqlBackup($sourceserver) + $null = $backup.Devices.Remove($backupDevice) + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -status "Complete" -Completed + Write-Message -Level Verbose -Message "Backup Completed for $dbname on $source." + + Write-Message -Level Verbose -Message "Running Restore Verify only on Backup of $dbname on $source." + try { + $restoreverify = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' + $restoreverify.Database = $dbname + $restoreverify.Devices.AddDevice($filename, $devicetype) + $result = $restoreverify.SqlVerify($sourceserver) + + if ($result -eq $false) { + Write-Message -Level Warning -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database." + continue + } + + Write-Message -Level Verbose -Message "Restore Verify Only for $filename succeeded." + } + catch { + Stop-Function -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database. Exception: $_" -Target $filename -ErrorRecord $_ -Continue + } + } + catch { + Stop-Function -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database. Exception: $_" -Target $filename -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating Automated Restore Job from Golden Backup for $dbname on $destination")) { + Write-Message -Level Verbose -Message "Creating Automated Restore Job from Golden Backup for $dbname on $destination." + try { + if ($force -eq $true -and $dbccgood -eq $false) { + $jobName = $jobname -replace "Rationalised", "DBCC ERROR" + } + + ## Create an agent job to restore the database + $job = New-Object Microsoft.SqlServer.Management.Smo.Agent.Job $jobServer, $jobname + $job.Name = $jobname + $job.OwnerLoginName = $jobowner + $job.Description = "This job will restore the $dbname database using the final backup located at $filename." + + ## Create a Job Category + if (!$jobServer.JobCategories[$categoryname]) { + New-SqlAgentJobCategory -JobServer $jobServer -categoryname $categoryname + } + + $job.Category = $categoryname + try { + if ($Pscmdlet.ShouldProcess($destination, "Creating Agent Job on $destination")) { + Write-Message -Level Verbose -Message "Created Agent Job $jobname on $destination." + $job.Create() + } + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job $jobname on $destination - aborting routine for this database." -Target $categoryname -ErrorRecord $_ -Continue + } + + ## Create Job Step + ## Aaron's Suggestion: In the restore script, add a comment block that tells the last known size of each file in the database. + ## Suggestion check for disk space before restore + ## Create Restore Script + try { + $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem $filename, 'FILE' + $restore.Devices.Add($device) + try { + $filelist = $restore.ReadFileList($destserver) + } + + catch { + throw 'File list could not be determined. This is likely due to connectivity issues or tiemouts with the Sql Server, the database version is incorrect, or the Sql Server service account does not have access to the file share. Script terminating.' + } + + $filestructure = Get-OfflineSqlFileStructure $destserver $dbname $filelist $ReuseSourceFolderStructure + + $jobStepCommand = Restore-Database $destserver $dbname $filename "Database" $filestructure -TSql -ErrorAction Stop + $jobStep = new-object Microsoft.SqlServer.Management.Smo.Agent.JobStep $job, $jobStepName + $jobStep.SubSystem = 'TransactSql' # 'PowerShell' + $jobStep.DatabaseName = 'master' + $jobStep.Command = $jobStepCommand + $jobStep.OnSuccessAction = 'QuitWithSuccess' + $jobStep.OnFailAction = 'QuitWithFailure' + if ($Pscmdlet.ShouldProcess($destination, "Creating Agent JobStep on $destination")) { + $null = $jobStep.Create() + } + $jobStartStepid = $jobStep.ID + Write-Message -Level Verbose -Message "Created Agent JobStep $jobStepName on $destination." + } + catch { + Stop-Function -Message "FAILED : To Create Agent JobStep $jobStepName on $destination - Aborting." -Target $jobStepName -ErrorRecord $_ -Continue + } + if ($Pscmdlet.ShouldProcess($destination, "Applying Agent Job $jobname to $destination")) { + $job.ApplyToTargetServer($destination) + $job.StartStepID = $jobStartStepid + $job.Alter() + } + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job $jobname on $destination - aborting routine for $dbname. Exception: $_" -Target $jobname -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping Database $dbname on $sourceserver")) { + ## Drop the database + try { + $null = Remove-DbaDatabase -SqlInstance $sourceserver -Database $dbname -Confirm:$false + Write-Message -Level Verbose -Message "Dropped $dbname Database on $source prior to running the Agent Job" + } + catch { + Stop-Function -Message "FAILED : To Drop database $dbname on $server - aborting routine for $dbname. Exception: $_" -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Running Agent Job on $destination to restore $dbname")) { + ## Run the restore job to restore it + Write-Message -Level Verbose -Message "Starting $jobname on $destination." + try { + $job = $destserver.JobServer.Jobs[$jobname] + $job.Start() + $job.Refresh() + $status = $job.CurrentRunStatus + + while ($status -ne 'Idle') { + Write-Message -Level Verbose -Message "Restore Job for $dbname on $destination is $status." + Start-Sleep -Seconds 15 + $job.Refresh() + $status = $job.CurrentRunStatus + } + + Write-Message -Level Verbose -Message "Restore Job $jobname has completed on $destination." + Write-Message -Level Verbose -Message "Sleeping for a few seconds to ensure the next step (DBCC) succeeds." + Start-Sleep -Seconds 10 ## This is required to ensure the next DBCC Check succeeds + } + catch { + Stop-Function -Message "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname. Exception: $_" -Continue + } + + if ($job.LastRunOutcome -ne 'Succeeded') { + # LOL, love the plug. + Write-Message -Level Warning -Message "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname." + Write-Message -Level Warning -Message "Check the Agent Job History on $destination - if you have SSMS2016 July release or later." + Write-Message -Level Warning -Message "Get-SqlAgentJobHistory -JobName '$jobname' -ServerInstance $destination -OutcomesType Failed." + continue + } + } + + $refreshRetries = 1 + + while ($null -eq ($destserver.databases[$dbname]) -and $refreshRetries -lt 6) { + Write-Message -Level verbose -Message "Database $dbname not found! Refreshing collection." + + #refresh database list, otherwise the next step (DBCC) can fail + $destserver.Databases.Refresh() + + Start-Sleep -Seconds 1 + + $refreshRetries += 1 + } + + + ## Run a Dbcc No choice here + if ($Pscmdlet.ShouldProcess($dbname, "Running Dbcc CHECKDB on $dbname on $destination")) { + Write-Message -Level Verbose -Message "Starting Dbcc CHECKDB for $dbname on $destination." + $null = Start-DbccCheck -Server $destserver -DbName $dbname + } + + if ($Pscmdlet.ShouldProcess($dbname, "Dropping Database $dbname on $destination")) { + ## Drop the database + try { + $null = Remove-DbaDatabase -SqlInstance $sourceserver -Database $dbname -Confirm:$false + Write-Message -Level Verbose -Message "Dropped $dbname database on $destination." + } + catch { + Stop-Function -Message "FAILED : To Drop database $dbname on $destination - Aborting. Exception: $_" -Target $dbname -ErrorRecord $_ -Continue + } + } + Write-Message -Level Verbose -Message "Rationalisation Finished for $dbname." + + [PSCustomObject]@{ + SqlInstance = $source + DatabaseName = $dbname + JobName = $jobname + TestingInstance = $destination + BackupFolder = $backupFolder + } + } + } + + end { + if (Test-FunctionInterrupt) { + return + } + if ($Pscmdlet.ShouldProcess("console", "Showing final message")) { + $End = Get-Date + Write-Message -Level Verbose -Message "Finished at $End." + $Duration = $End - $start + Write-Message -Level Verbose -Message "Script Duration: $Duration." + } + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Remove-SqlDatabaseSafely + } +} +function Remove-DbaDbCertificate { + <# +.SYNOPSIS +Deletes specified database certificate + +.DESCRIPTION +Deletes specified database certificate + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the certificate will be removed. + +.PARAMETER Certificate +The certificate that will be removed + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER InputObject +Piped certificate objects + +.NOTES +Tags: Certificate +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Remove-DbaDbCertificate -SqlInstance Server1 + +The certificate in the master database on server1 will be removed if it exists. + +.EXAMPLE +Remove-DbaDbCertificate -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to remove the certificate in the 'db1' database and drops the key. + + +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [object[]]$Database, + [parameter(Mandatory, ParameterSetName = "instance")] + [object[]]$Certificate, + [parameter(ValueFromPipeline, ParameterSetName = "collection")] + [Microsoft.SqlServer.Management.Smo.Certificate[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Remove-DbaDatabaseCertificate + + function drop-cert ($smocert) { + $server = $smocert.Parent.Parent + $instance = $server.DomainInstanceName + $cert = $smocert.Name + $db = $smocert.Parent.Name + + $output = [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $instance + Database = $db + Certificate = $cert + Status = $null + } + + if ($Pscmdlet.ShouldProcess($instance, "Dropping the certificate named $cert for database '$db' on $server")) { + try { + $smocert.Drop() + Write-Message -Level Verbose -Message "Successfully removed certificate named $cert from the $db database on $server" + $output.status = "Success" + } + catch { + $output.Status = "Failure" + Stop-Function -Message "Failed to drop certificate named $cert from $db on $server." -Target $smocert -InnerErrorRecord $_ -Continue + } + $output + } + } + } + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + $currentdb = $server.Databases[$db] + + if ($null -eq $currentdb) { + Stop-Function -Message "Database '$db' does not exist on $server" -Target $currentdb -Continue + } + + if (-not $currentdb.IsAccessible) { + Stop-Function -Message "Database '$db' is not accessible" -Target $currentdb -Continue + } + + foreach ($cert in $certificate) { + $smocert = $currentdb.Certificates[$cert] + + if ($null -eq $smocert) { + Stop-Function -Message "No certificate named $cert exists in the $db database on $server" -Target $currentdb.Certificates -Continue + } + + Drop-Cert -smocert $smocert + } + } + } + + foreach ($smocert in $InputObject) { + Drop-Cert -smocert $smocert + } + } +} +#ValidationTags#FlowControl# +function Remove-DbaDbSnapshot { + <# + .SYNOPSIS + Removes database snapshots + + .DESCRIPTION + Removes (drops) database snapshots from the server + + .PARAMETER SqlInstance + The SQL Server that you're connecting to + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + Removes snapshots for only this specific base db + + .PARAMETER ExcludeDatabase + Removes snapshots excluding this specific base dbs + + .PARAMETER Snapshot + Restores databases from snapshot with this name only + + .PARAMETER AllSnapshots + Specifies that you want to remove all snapshots from the server + + .PARAMETER Force + Will forcibly kill all running queries that prevent the drop process. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER PipelineSnapshot + Internal parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDbSnapshot + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a + + Removes all database snapshots from sqlserver2014a + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snap_20161201, HR_snap_20161101 + + Removes database snapshots named HR_snap_20161201 and HR_snap_20161101 + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Removes all database snapshots having HR and Accounting as base dbs + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snapshot, Accounting_snapshot + + Removes HR_snapshot and Accounting_snapshot + + .EXAMPLE + Get-DbaDbSnapshot -SqlInstance sql2016 | Where SnapshotOf -like '*dumpsterfire*' | Remove-DbaDbSnapshot + + Removes all snapshots associated with databases that have dumpsterfire in the name + +#> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Snapshot, + [parameter(ValueFromPipeline = $true)] + [object]$PipelineSnapshot, + [switch]$AllSnapshots, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + if (!$Snapshot -and !$Database -and !$AllSnapshots -and $null -eq $PipelineSnapshot -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify -Snapshot, -Database, -Exclude or -AllSnapshots" + return + } + # handle the database object passed by the pipeline + # do we need a specialized type back ? + if ($null -ne $PipelineSnapshot -and $PipelineSnapshot.getType().Name -eq 'pscustomobject') { + if ($Pscmdlet.ShouldProcess($PipelineSnapshot.SnapshotDb.Parent.DomainInstanceName, "Remove db snapshot $($PipelineSnapshot.SnapshotDb.Name)")) { + try { + $server = $PipelineSnapshot.SnapshotDb.Parent + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + try { + if ($Force) { + $server.KillAllProcesses($PipelineSnapshot.SnapshotDb.Name) + } + $null = $server.Query("drop database [$($PipelineSnapshot.SnapshotDb.Name)]") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $status = "Drop failed" + } + + [PSCustomObject]@{ + ComputerName = $PipelineSnapshot.SnapshotDb.Parent.NetName + InstanceName = $PipelineSnapshot.SnapshotDb.Parent.ServiceName + SqlInstance = $PipelineSnapshot.SnapshotDb.Parent.DomainInstanceName + Database = $PipelineSnapshot.Database + SnapshotOf = $PipelineSnapshot.SnapshotOf + Status = $status + } + } + return + } + + # if piped value either doesn't exist or is not the proper type + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + + $alldbq = @" +SELECT sn.name AS Name, dt.name AS DatabaseSnapshotBaseName, sn.create_date AS CreateDate, +CASE WHEN sn.state = 6 THEN 0 ELSE 1 END AS IsAccessible, +CASE WHEN sn.source_database_id IS NOT NULL THEN 1 ELSE 0 END AS IsDatabaseSnapshot +FROM sys.databases sn +LEFT JOIN sys.databases dt +ON sn.source_database_id = dt.database_id +"@ + $dbs = $server.Query($alldbq) + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.DatabaseSnapshotBaseName } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.DatabaseSnapshotBaseName } + } + + if ($Snapshot) { + $dbs = $dbs | Where-Object { $Snapshot -contains $_.Name } + } + + if (!$Snapshot -and !$Database) { + $dbs = $dbs | Where-Object IsDatabaseSnapshot -eq $true | Sort-Object DatabaseSnapshotBaseName, Name + } + + + foreach ($dbraw in $dbs) { + if ($dbraw.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "Database $dbraw is not accessible." + continue + } + $db = $server.Databases[$($dbraw.Name)] + if ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $db")) { + $basedb = $db.DatabaseSnapshotBaseName + try { + if ($Force) { + # cannot drop the snapshot if someone is using it + $server.KillAllProcesses($db) + } + $null = $server.Query("drop database $db") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $status = "Drop failed" + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + SnapshotOf = $basedb + Status = $status + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Remove-DbaDatabaseSnapshot + } +} +function Remove-DbaDbUser { + <# + .SYNOPSIS + Drop database user + + .DESCRIPTION + If user is the owner of a schema with the same name and if if the schema does not have any underlying objects the schema will be + dropped. If user owns more than one schema, the owner of the schemas that does not have the same name as the user, will be + changed to 'dbo'. If schemas have underlying objects, you must specify the -Force parameter so the user can be dropped. + + .PARAMETER SqlInstance + The SQL Instances that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER User + Specifies the list of users to remove. + + .PARAMETER InputObject + Support piping from Get-DbaDatabaseUser. + + .PARAMETER Force + If enabled this will force the change of the owner to 'dbo' for any schema which owner is the User. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, User, Login, Security + Author: Doug Meyers (@dgmyrs) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDbUser + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -User user1 + + Drops user1 from all databases it exists in on server 'sqlserver2014'. + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -Database database1 -User user1 + + Drops user1 from the database1 database on server 'sqlserver2014'. + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -ExcludeDatabase model -User user1 + + Drops user1 from all databases it exists in on server 'sqlserver2014' except for the model database. + + .EXAMPLE + Get-DbaDatabaseUser sqlserver2014 | Where-Object Name -In "user1" | Remove-DbaDbUser + + Drops user1 from all databases it exists in on server 'sqlserver2014'. + +#> + + [CmdletBinding(DefaultParameterSetName = 'User', SupportsShouldProcess = $true)] + Param ( + [parameter(Position = 1, Mandatory, ValueFromPipeline, ParameterSetName = 'User')] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + + [parameter(ParameterSetName = 'User')] + [Alias("Credential")] + [PSCredential] + $SqlCredential, + + [parameter(ParameterSetName = 'User')] + [Alias("Databases")] + [object[]]$Database, + + [parameter(ParameterSetName = 'User')] + [object[]]$ExcludeDatabase, + + [parameter(Mandatory, ParameterSetName = 'User')] + [object[]]$User, + + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')] + [Microsoft.SqlServer.Management.Smo.User[]]$InputObject, + + [parameter(ParameterSetName = 'User')] + [parameter(ParameterSetName = 'Object')] + [switch]$Force, + + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Remove-DbUser { + [CmdletBinding()] + param ([Microsoft.SqlServer.Management.Smo.User[]]$users) + + foreach ($user in $users) { + $db = $user.Parent + $server = $db.Parent + $ownedObjects = $false + Write-Message -Level Verbose -Message "Removing User $user from Database $db on target $server" + + # Drop Schemas owned by the user before droping the user + $schemaUrns = $user.EnumOwnedObjects() | Where-Object Type -EQ Schema + if ($schemaUrns) { + Write-Message -Level Verbose -Message "User $user owns $($schemaUrns.Count) schema(s)." + + # Need to gather up the schema changes so they can be done in a non-desctructive order + $alterSchemas = @() + $dropSchemas = @() + + foreach ($schemaUrn in $schemaUrns) { + $schema = $server.GetSmoObject($schemaUrn) + + # Drop any schema that is the same name as the user + if ($schema.Name -EQ $user.Name) { + # Check for owned objects early so we can exit before any changes are made + $ownedUrns = $schema.EnumOwnedObjects() + if (-Not $ownedUrns) { + $dropSchemas += $schema + } + else { + Write-Message -Level Warning -Message "User owns objects in the database and will not be removed." + foreach ($ownedUrn in $ownedUrns) { + $obj = $server.GetSmoObject($ownedUrn) + Write-Message -Level Warning -Message "User $user owns $($obj.GetType().Name) $obj" + } + $ownedObjects = $true + } + } + + # Change the owner of any schema not the same name as the user + if ($schema.Name -NE $user.Name) { + # Check for owned objects early so we can exit before any changes are made + $ownedUrns = $schema.EnumOwnedObjects() + if (($ownedUrns -And $Force) -Or (-Not $ownedUrns)) { + $alterSchemas += $schema + } + else { + Write-Message -Level Warning -Message "User $user owns the Schema $schema, which owns $($ownedUrns.Count) Object(s). If you want to change the schemas' owner to [dbo] and drop the user anyway, use -Force parameter. User $user will not be removed." + $ownedObjects = $true + } + } + } + } + + if (-Not $ownedObjects) { + try { + # Alter Schemas + foreach ($schema in $alterSchemas) { + Write-Message -Level Verbose -Message "Owner of Schema $schema will be changed to [dbo]." + if ($PSCmdlet.ShouldProcess($server, "Change the owner of Schema $schema to [dbo].")) { + $schema.Owner = "dbo" + $schema.Alter() + } + } + + # Drop Schemas + foreach ($schema in $dropSchemas) { + if ($PSCmdlet.ShouldProcess($server, "Drop Schema $schema from Database $db.")) { + $schema.Drop() + } + } + + # Finally, Drop user + if ($PSCmdlet.ShouldProcess($server, "Drop User $user from Database $db.")) { + $user.Drop() + } + + $status = "Dropped" + + } + catch { + Write-Error -Message "Could not drop $user from Database $db on target $server" + $status = "Not Dropped" + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + User = $user + Status = $status + } + } + } + } + } + + process { + if ($InputObject) { + Remove-DbUser $InputObject + } + else { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + Write-Message -Level Verbose -Message "Get users in Database $db on target $server" + $users = Get-DbaDatabaseUser -SqlInstance $server -Database $db.Name + $users = $users | Where-Object Name -In $User + Remove-DbUser $users + } + } + } + } + +} +function Remove-DbaLogin { + <# +.SYNOPSIS +Drops a Login + +.DESCRIPTION +Tries a bunch of different ways to remove a Login or two or more. + +.PARAMETER SqlInstance +The SQL Server instance holding the Logins to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + +.PARAMETER SqlCredential +Allows you to login to servers using alternative credentials. + +.PARAMETER Login +The Login(s) to process - this list is auto-populated from the server. If unspecified, all Logins will be processed. + +.PARAMETER InputObject +A collection of Logins (such as returned by Get-DbaLogin), to be removed. + +.PARAMETER Force +Kills any sessions associated with the login prior to drop + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Delete, Logins + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaLogin + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin + +Prompts then removes the Login mylogin on SQL Server sql2016 + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin, yourlogin + +Prompts then removes the Logins mylogin and yourlogin on SQL Server sql2016 + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin -Confirm:$false + +Does not prompt and swiftly removes mylogin on SQL Server sql2016 + +.EXAMPLE +Get-DbaLogin -SqlInstance server\instance -Login yourlogin | Remove-DbaLogin + +removes mylogin on SQL Server server\instance + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(Mandatory = $false)] + [Alias("Credential")] + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [string[]]$Login, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "Logins")] + [Microsoft.SqlServer.Management.Smo.Login[]]$InputObject, + [switch]$Force, + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Logins | Where-Object { $_.Name -in $Login } + } + + foreach ($currentlogin in $InputObject) { + try { + $server = $currentlogin.Parent + if ($Pscmdlet.ShouldProcess("$currentlogin on $server", "KillLogin")) { + if ($force) { + $null = Stop-DbaProcess -SqlInstance $server -Login $currentlogin + } + + $currentlogin.Drop() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Login = $currentlogin.name + Status = "Dropped" + } + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Login = $currentlogin.name + Status = $_ + } + Stop-Function -Message "Could not drop Login $currentlogin on $server" -ErrorRecord $_ -Target $currentlogin -Continue + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Remove-DbaNetworkCertificate { + <# + .SYNOPSIS + Removes the network certificate for SQL Server instance + + .DESCRIPTION + Removes the network certificate for SQL Server instance. This setting is found in Configuration Manager. + + .PARAMETER SqlInstance + The target SQL Server - defaults to localhost. If target is a cluster, you must also specify InstanceClusterName (see below) + + .PARAMETER Credential + Allows you to login to the computer (not sql instance) using alternative credentials. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaNetworkCertificate + + Removes the Network Certificate for the default instance (MSSQLSERVER) on localhost + + .EXAMPLE + Remove-DbaNetworkCertificate -SqlInstance sql1\SQL2008R2SP2 + + Removes the Network Certificate for the SQL2008R2SP2 instance on sql1 + + .EXAMPLE + Remove-DbaNetworkCertificate -SqlInstance localhost\SQL2008R2SP2 -WhatIf + + Shows what would happen if the command were run + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low", DefaultParameterSetName = 'Default')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [PSCredential] + + $Credential, + + [switch] + [Alias('Silent')]$EnableException + ) + process { + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname" + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance" -Target $instance -Continue -Category InvalidArgument + } + + Write-Message -Level Output -Message "Connecting to SQL WMI on $($instance.ComputerName)" + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($($instance.InstanceName))" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance" -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$($args[0])\MSSQLServer\SuperSocketNetLib" + $cert = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + Set-ItemProperty -Path $regpath -Name Certificate -Value $null + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + RemovedThumbprint = $cert.Thumbprint + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $ComputerName to remove the cert")) { + try { + Invoke-Command2 -ComputerName $resolved.fqdn -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.fqdn) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaOrphanUser { + <# + .SYNOPSIS + Drop orphan users with no existing login to map + + .DESCRIPTION + An orphan user is defined by a user that does not have their matching login. (Login property = ""). + + If user is the owner of the schema with the same name and if if the schema does not have any underlying objects the schema will be dropped. + + If user owns more than one schema, the owner of the schemas that does not have the same name as the user, will be changed to 'dbo'. If schemas have underlying objects, you must specify the -Force parameter so the user can be dropped. + + If exists a login to map the drop will not be performed unless you specify the -Force parameter (only when calling from Repair-DbaOrphanUser. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER User + Specifies the list of users to remove. + + .PARAMETER Force + If this switch is enabled: + If exists any schema which owner is the User, this will force the change of the owner to 'dbo'. + If exists a login to map the drop will not be performed unless you specify this parameter. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Orphan, Database, Security, Login + Author: Claudio Silva (@ClaudioESSilva) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaOrphanUser + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sql2005 + + Finds and drops all orphan users without matching Logins in all databases present on server 'sql2005'. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -SqlCredential $cred + + Finds and drops all orphan users without matching Logins in all databases present on server 'sqlserver2014a'. SQL Server authentication will be used in connecting to the server. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1, db2 -Force + + Finds and drops orphan users even if they have a matching Login on both db1 and db2 databases. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -ExcludeDatabase db1, db2 -Force + + Finds and drops orphan users even if they have a matching Login from all databases except db1 and db2. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -User OrphanUser + + Removes user OrphanUser from all databases only if there is no matching login. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -User OrphanUser -Force + + Removes user OrphanUser from all databases even if they have a matching Login. Any schema that the user owns will change ownership to dbo. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [object[]]$User, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($Instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $Instance." + try { + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Can't connect to $Instance or access denied. Skipping." + continue + } + + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + $CallStack = Get-PSCallStack | Select-Object -Property * + if ($CallStack.Count -eq 1) { + $StackSource = $CallStack[0].Command + } + else { + #-2 because index base is 0 and we want the one before the last (the last is the actual command) + $StackSource = $CallStack[($CallStack.Count - 2)].Command + } + + if ($DatabaseCollection) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + + if ($StackSource -eq "Repair-DbaOrphanUser") { + Write-Message -Level Verbose -Message "Call origin: Repair-DbaOrphanUser." + #Will use collection from parameter ($User) + } + else { + Write-Message -Level Verbose -Message "Validating users on database $db." + + if ($User.Count -eq 0) { + #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $User = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + } + else { + + #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $User = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($User -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + + } + } + + if ($User.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found." + foreach ($dbuser in $User) { + $SkipUser = $false + + $ExistLogin = $null + + if ($StackSource -ne "Repair-DbaOrphanUser") { + #Need to validate Existing Login because the call does not came from Repair-DbaOrphanUser + $ExistLogin = $server.logins | Where-Object { + $_.Isdisabled -eq $False -and + $_.IsSystemObject -eq $False -and + $_.IsLocked -eq $False -and + $_.Name -eq $dbuser.Name + } + } + + #Schemas only appears on SQL Server 2005 (v9.0) + if ($server.versionMajor -gt 8) { + + #reset variables + $AlterSchemaOwner = "" + $DropSchema = "" + + #Validate if user owns any schema + $Schemas = @() + $Schemas = $db.Schemas | Where-Object Owner -eq $dbuser.Name + + if (@($Schemas).Count -gt 0) { + Write-Message -Level Verbose -Message "User $dbuser owns one or more schemas." + + foreach ($sch in $Schemas) { + <# + On sql server 2008 or lower the EnumObjects method does not accept empty parameter. + 0x1FFFFFFF is the way we can say we want everything known by those versions + + When it is an higher version we can use empty to get all + #> + if ($server.versionMajor -lt 11) { + $NumberObjects = ($db.EnumObjects(0x1FFFFFFF) | Where-Object { $_.Schema -eq $sch.Name } | Measure-Object).Count + } + else { + $NumberObjects = ($db.EnumObjects() | Where-Object { $_.Schema -eq $sch.Name } | Measure-Object).Count + } + + if ($NumberObjects -gt 0) { + if ($Force) { + Write-Message -Level Verbose -Message "Parameter -Force was used! The schema '$($sch.Name)' have $NumberObjects underlying objects. We will change schema owner to 'dbo' and drop the user." + + if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'. -Force used.")) { + $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "ALTER OWNER" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "dbo" + } + } + } + else { + Write-Message -Level Warning -Message "Schema '$($sch.Name)' owned by user $($dbuser.Name) have $NumberObjects underlying objects. If you want to change the schemas' owner to 'dbo' and drop the user anyway, use -Force parameter. Skipping user '$dbuser'." + $SkipUser = $true + break + } + } + else { + if ($sch.Name -eq $dbuser.Name) { + Write-Message -Level Verbose -Message "The schema '$($sch.Name)' have the same name as user $dbuser. Schema will be dropped." + + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping schema '$($sch.Name)'.")) { + $DropSchema += "DROP SCHEMA [$($sch.Name)]" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "DROP" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "N/A" + } + } + } + else { + Write-Message -Level Warning -Message "Schema '$($sch.Name)' does not have any underlying object. Ownership will be changed to 'dbo' so the user can be dropped. Remember to re-check permissions on this schema!" + + if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'.")) { + $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "ALTER OWNER" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "dbo" + } + } + } + } + } + + } + else { + Write-Message -Level Verbose -Message "User $dbuser does not own any schema. Will be dropped." + } + + $query = "$AlterSchemaOwner `r`n$DropSchema `r`nDROP USER " + $dbuser + + Write-Message -Level Debug -Message $query + } + else { + $query = "EXEC master.dbo.sp_droplogin @loginame = N'$($dbuser.name)'" + } + + if ($ExistLogin) { + if (-not $SkipUser) { + if ($Force) { + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user $dbuser using -Force")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User $dbuser was dropped from $($db.Name). -Force parameter was used!" + } + } + else { + Write-Message -Level Warning -Message "Orphan user $($dbuser.Name) has a matching login. The user will not be dropped. If you want to drop anyway, use -Force parameter." + Continue + } + } + } + else { + if (-not $SkipUser) { + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user $dbuser")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User $dbuser was dropped from $($db.Name)." + } + } + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database $db." + } + #reset collection + $User = $null + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $db -Continue + } + } + } + else { + Write-Message -Level Verbose -Message "There are no databases to analyse." + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Remove-SqlOrphanUser + } +} +function Remove-DbaPfDataCollectorCounter { + <# + .SYNOPSIS + Removes a Performance Data Collector Counter. + + .DESCRIPTION + Removes a Performance Data Collector Counter. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The name of the Collector Set to search. + + .PARAMETER Collector + The name of the Collector to remove. + + .PARAMETER Counter + The name of the Counter - in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaPfDataCollectorCounter + + .EXAMPLE + Remove-DbaPfDataCollectorCounter -ComputerName sql2017 -CollectorSet 'System Correlation' -Collector DataCollector01 -Counter '\LogicalDisk(*)\Avg. Disk Queue Length' + + Prompts for confirmation then removes the '\LogicalDisk(*)\Avg. Disk Queue Length' counter within the DataCollector01 collector within the System Correlation collector set on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter | Out-GridView -PassThru | Remove-DbaPfDataCollectorCounter -Confirm:$false + + Allows you to select which counters you'd like on localhost and does not prompt for confirmation. + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("Name")] + [object[]]$Counter, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $setscript = { + $setname = $args[0]; $removexml = $args[1] + $CollectorSet = New-Object -ComObject Pla.DataCollectorSet + $CollectorSet.SetXml($removexml) + $CollectorSet.Commit($setname, $null, 0x0003) #add or modify. + $CollectorSet.Query($setname, $Null) + } + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorCounter -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector -Counter $Counter + } + } + + if ($InputObject) { + if (-not $InputObject.CounterObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorCounter." + return + } + } + + foreach ($object in $InputObject) { + $computer = $InputObject.ComputerName + $null = Test-ElevationRequirement -ComputerName $computer -Continue + $setname = $InputObject.DataCollectorSet + $collectorname = $InputObject.DataCollector + + $xml = [xml]($InputObject.DataCollectorSetXml) + + foreach ($countername in $counter) { + $node = $xml.SelectSingleNode("//Name[.='$collectorname']").SelectSingleNode("//Counter[.='$countername']") + $null = $node.ParentNode.RemoveChild($node) + $node = $xml.SelectSingleNode("//Name[.='$collectorname']").SelectSingleNode("//CounterDisplayName[.='$countername']") + $null = $node.ParentNode.RemoveChild($node) + } + + $plainxml = $xml.OuterXml + + if ($Pscmdlet.ShouldProcess("$computer", "Remove $countername from $collectorname with the $setname collection set")) { + try { + $results = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $setname, $plainxml -ErrorAction Stop -Raw + Write-Message -Level Verbose -Message " $results" + [pscustomobject]@{ + ComputerName = $computer + DataCollectorSet = $setname + DataCollector = $collectorname + Name = $counterName + Status = "Removed" + } + } + catch { + Stop-Function -Message "Failure importing $Countername to $computer." -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} +function Remove-DbaPfDataCollectorSet { + <# + .SYNOPSIS + Removes a Performance Monitor Data Collector Set + + .DESCRIPTION + Removes a Performance Monitor Data Collector Set. When removing data collector sets from the local instance, Run As Admin is required. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The name of the Collector Set to remove. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaPfDataCollectorSet + + .EXAMPLE + Remove-DbaPfDataCollectorSet + + Prompts for confirmation then removes all ready Collectors on localhost. + + .EXAMPLE + Remove-DbaPfDataCollectorSet -ComputerName sql2017 -Confirm:$false + + Attempts to remove all ready Collectors on localhost and does not prompt to confirm. + + .EXAMPLE + Remove-DbaPfDataCollectorSet -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Prompts for confirmation then removes the 'System Correlation' Collector on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Remove-DbaPfDataCollectorSet + + Removes the 'System Correlation' Collector. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Stop-DbaPfDataCollectorSet | Remove-DbaPfDataCollectorSet + + Stops and removes the 'System Correlation' Collector. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param ( + [DbaInstance[]]$ComputerName=$env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $setscript = { + $setname = $args + $collectorset = New-Object -ComObject Pla.DataCollectorSet + $collectorset.Query($setname, $null) + if ($collectorset.name -eq $setname) { + $null = $collectorset.Delete() + } + else { + Write-Warning "Data Collector Set $setname does not exist on $env:COMPUTERNAME." + } + } + } + process { + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorSet -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorSetObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorSet." + return + } + } + + # Check to see if its running first + foreach ($set in $InputObject) { + $setname = $set.Name + $computer = $set.ComputerName + $status = $set.State + + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + Write-Message -Level Verbose -Message "$setname on $ComputerName is $status." + + if ($status -eq "Running") { + Stop-Function -Message "$setname on $computer is running. Use Stop-DbaPfDataCollectorSet to stop first." -Continue + } + + if ($Pscmdlet.ShouldProcess("$computer", "Removing collector set $setname")) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $setname -ErrorAction Stop + [pscustomobject]@{ + ComputerName = $computer + Name = $setname + Status = "Removed" + } + } + catch { + Stop-Function -Message "Failure Removing $setname on $computer." -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} +#ValidationTags#FlowControl,Pipeline# +function Remove-DbaSpn { + <# +.SYNOPSIS +Removes an SPN for a given service account in active directory and also removes delegation to the same SPN, if found + +.DESCRIPTION +This function will connect to Active Directory and search for an account. If the account is found, it will attempt to remove the specified SPN. Once the SPN is removed, the function will also remove delegation to that service. + +In order to run this function, the credential you provide must have write access to Active Directory. + +Note: This function supports -WhatIf + +.PARAMETER SPN +The SPN you want to remove + +.PARAMETER ServiceAccount +The account you want the SPN remove from + +.PARAMETER Credential +The credential you want to use to connect to Active Directory to make the changes + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Confirm +Turns confirmations before changes on or off + +.PARAMETER WhatIf +Shows what would happen if the command was executed + +.NOTES +Tags: SPN +Author: Drew Furgiuele (@pittfurg), http://www.port1433.com + +dbatools PowerShell module (https://dbatools.io) +Copyright (C) 2016 Chrissy LeMaire +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaSpn + +.EXAMPLE +Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account + +Connects to Active Directory and removes a provided SPN from the given account (and also the relative delegation) + +.EXAMPLE +Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -EnableException + +Connects to Active Directory and removes a provided SPN from the given account, suppressing all error messages and throw exceptions that can be caught instead + +.EXAMPLE +Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -Credential (Get-Credential) + +Connects to Active Directory and removes a provided SPN to the given account. Uses alternative account to connect to AD. + +.EXAMPLE +Test-DbaSpn -ComputerName sql2005 | Where { $_.isSet -eq $true } | Remove-DbaSpn -WhatIf + +Shows what would happen trying to remove all set SPNs for sql2005 and the relative delegations + +.EXAMPLE +Test-DbaSpn -ComputerName sql2005 | Where { $_.isSet -eq $true } | Remove-DbaSpn + +Removes all set SPNs for sql2005 and the relative delegations + + +#> + [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("RequiredSPN")] + [string]$SPN, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("InstanceServiceAccount", "AccountName")] + [string]$ServiceAccount, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + Write-Message -Message "Looking for account $ServiceAccount..." -Level Verbose + $searchfor = 'User' + if ($ServiceAccount.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $ServiceAccount -Type $searchfor -Credential $Credential -EnableException + } + catch { + Stop-Function -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($ServiceAccount). $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + if ($Result.Count -gt 0) { + try { + $adentry = $Result.GetUnderlyingObject() + } + catch { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has been found, but you don't have enough permission to inspect its properties $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + } + else { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has not been found" -EnableException $EnableException -Target $ServiceAccount + } + + # Cool! Remove an SPN + $delegate = $true + $spnadobject = $adentry.Properties['servicePrincipalName'] + + if ($spnadobject -notcontains $spn) { + Write-Message -Level Warning -Message "SPN $SPN not found" + $status = "SPN not found" + $set = $false + } + + if ($PSCmdlet.ShouldProcess("$spn", "Removing SPN for service account")) { + try { + if ($spnadobject -contains $spn) { + $null = $spnadobject.Remove($spn) + $adentry.CommitChanges() + Write-Message -Message "Remove SPN $spn for $serviceaccount" -Level Verbose + $set = $false + $status = "Successfully removed SPN" + } + } + catch { + Write-Message -Message "Could not remove SPN. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccountWrite + $set = $true + $status = "Failed to remove SPN" + $delegate = $false + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "servicePrincipalName" + IsSet = $set + Notes = $status + } + } + # if we removed the SPN, we should clean up also the delegation + if ($PSCmdlet.ShouldProcess("$spn", "Removing delegation for service account for SPN")) { + # if we didn't remove the SPN we shouldn't do anything + if ($delegate) { + # even if we removed the SPN, delegation could have been not set at all. We should not raise an error + if ($adentry.Properties['msDS-AllowedToDelegateTo'] -notcontains $spn) { + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $false + Notes = "Delegation not found" + } + } + else { + # we indeed need the cleanup + try { + $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Remove($spn) + $adentry.CommitChanges() + Write-Message -Message "Removed kerberos delegation $spn for $ServiceAccount" -Level Verbose + $set = $false + $status = "Successfully removed delegation" + } + catch { + Write-Message -Message "Could not remove delegation. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $true + $status = "Failed to remove delegation" + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $set + Notes = $status + } + } + } + + } + } +} +function Remove-DbaTrace { + <# + .SYNOPSIS + Stops and closes the specified trace and deletes its definition from the server. + + .DESCRIPTION + Stops and closes the specified trace and deletes its definition from the server. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Id + A list of trace ids + + .PARAMETER InputObject + Internal parameter for piping + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Remove-DbaTrace -SqlInstance sql2008 + + Stops and removes all traces on sql2008 + + .EXAMPLE + Remove-DbaTrace -SqlInstance sql2008 -Id 1 + + Stops and removes all trace with ID 1 on sql2008 + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2008 | Out-GridView -PassThru | Remove-DbaTrace + + Stops and removes selected traces on sql2008 + +#> + [CmdletBinding()] + Param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int[]]$Id, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + if (-not $InputObject -and $SqlInstance) { + $InputObject = Get-DbaTrace -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Id $Id + } + + foreach ($trace in $InputObject) { + if (-not $trace.id -and -not $trace.Parent) { + Stop-Function -Message "Input is of the wrong type. Use Get-DbaTrace." -Continue + return + } + + $server = $trace.Parent + $traceid = $trace.id + $default = Get-DbaTrace -SqlInstance $server -Default + + if ($default.id -eq $traceid) { + Stop-Function -Message "The default trace on $server cannot be stopped. Use Set-DbaSpConfigure to turn it off." -Continue + } + + $stopsql = "sp_trace_setstatus $traceid, 0" + $removesql = "sp_trace_setstatus $traceid, 2" + + try { + $server.Query($stopsql) + if (Get-DbaTrace -SqlInstance $server -Id $traceid) { + $server.Query($removesql) + } + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Id = $traceid + Status = "Stopped, closed and deleted" + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + return + } + } + } +} +function Remove-DbaXESession { + <# + .SYNOPSIS + Removes Extended Events sessions. + + .DESCRIPTION + This script removes Extended Events sessions on a SQL Server instance. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Specifies a list of Extended Events sessions to remove. + + .PARAMETER AllSessions + If this switch is enabled, all Extended Events sessions will be removed except the packaged sessions AlwaysOn_health, system_health, telemetry_xevents. + + .PARAMETER InputObject + Accepts a collection of XEsession objects as output by Get-DbaXESession. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaXESession + + .EXAMPLE + Remove-DbaXESession -SqlInstance sql2012 -AllSessions + + Removes all Extended Event Session on the sqlserver2014 instance. + + .EXAMPLE + Remove-DbaXESession -SqlInstance sql2012 -Session xesession1,xesession2 + + Removes the xesession1 and xesession2 Extended Event sessions. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2017 | Remove-DbaXESession -Confirm:$false + + Removes all sessions from sql2017, bypassing prompts. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2012 -Session xesession1 | Remove-DbaXESession + + Removes the sessions returned from the Get-DbaXESession function. + #> + [CmdletBinding(DefaultParameterSetName = 'Session', SupportsShouldProcess, ConfirmImpact = 'High')] + param ( + [parameter(Position = 1, Mandatory, ParameterSetName = 'Session')] + [parameter(Position = 1, Mandatory, ParameterSetName = 'All')] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(ParameterSetName = 'Session')] + [parameter(ParameterSetName = 'All')] + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = 'Session')] + [Alias("Sessions")] + [object[]]$Session, + [parameter(Mandatory, ParameterSetName = 'All')] + [switch]$AllSessions, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')] + [Microsoft.SqlServer.Management.XEvent.Session[]]$InputObject, + [switch]$EnableException + ) + + begin { + # Remove each XESession + function Remove-XESessions { + [CmdletBinding()] + param ([Microsoft.SqlServer.Management.XEvent.Session[]]$xeSessions) + + foreach ($xe in $xeSessions) { + $instance = $xe.Parent.Name + $session = $xe.Name + + if ($Pscmdlet.ShouldProcess("$instance", "Removing XEvent Session $session")) { + try { + $xe.Drop() + [pscustomobject]@{ + ComputerName = $xe.Parent.NetName + InstanceName = $xe.Parent.ServiceName + SqlInstance = $xe.Parent.DomainInstanceName + Session = $session + Status = "Removed" + } + } + catch { + Stop-Function -Message "Could not remove XEvent Session on $instance" -Target $session -ErrorRecord $_ -Continue + } + } + } + } + } + + process { + if ($InputObject) { + # avoid the collection issue + $sessions = Get-DbaXESession -SqlInstance $InputObject.Parent -Session $InputObject.Name + foreach ($item in $sessions) { + Remove-XESessions $item + } + } + else { + foreach ($instance in $SqlInstance) { + $xeSessions = Get-DbaXESession -SqlInstance $instance -SqlCredential $SqlCredential + + # Filter xeSessions based on parameters + if ($Session) { + $xeSessions = $xeSessions | Where-Object { $_.Name -in $Session } + } + elseif ($AllSessions) { + $systemSessions = @('AlwaysOn_health', 'system_health', 'telemetry_xevents') + $xeSessions = $xeSessions | Where-Object { $_.Name -notin $systemSessions } + } + + Remove-XESessions $xeSessions + } + } + } +} +function Remove-DbaXESmartTarget { + <# + .SYNOPSIS + Removes an XESmartTarget PowerShell Job. + + .DESCRIPTION + Removes an XESmartTarget PowerShell Job. + + .PARAMETER InputObject + Specifies one or more XESmartTarget job objects as output by Get-DbaXESmartTarget. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/Remove-DbaXESmartTarget + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + Get-DbaXESmartTarget | Remove-DbaXESmartTarget + + Removes all XESmartTarget jobs. + + .EXAMPLE + Get-DbaXESmartTarget | Where-Object Id -eq 2 | Remove-DbaXESmartTarget + + Removes a specific XESmartTarget job. + #> + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + if ($Pscmdlet.ShouldProcess("localhost", "Removing job $id")) { + try { + $id = $InputObject.Id + Write-Message -Level Output -Message "Removing job $id, this may take a couple minutes." + Get-Job -ID $InputObject.Id | Remove-Job -Force + Write-Message -Level Output -Message "Successfully removed $id." + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } + } +} +function Rename-DbaDatabase { + <# + .SYNOPSIS + Changes database name, logical file names, file group names and physical file names (optionally handling the move). BETA VERSION. + + .DESCRIPTION + Can change every database metadata that can be renamed. + The ultimate goal is choosing to have a default template to enforce in your environment + so your naming convention for every bit can be put in place in no time. + The process is as follows (it follows the hierarchy of the entities): + - database name is changed (optionally, forcing users out) + - filegroup name(s) are changed accordingly + - logical name(s) are changed accordingly + - physical file(s) are changed accordingly + - if Move is specified, the database will be taken offline and the move will initiate, then it will be taken online + - if Move is not specified, the database remains online (unless SetOffline), and you are in charge of moving files + If any of the above fails, the process stops. + Please take a backup of your databases BEFORE using this, and remember to backup AFTER (also a FULL backup of master) + + It returns an object for each database with all the renames done, plus hidden properties showing a "human" representation of them. + + It's better you store the resulting object in a variable so you can inspect it in case of issues, e.g. "$result = Rename-DbaDatabase ....." + + To get a grasp without worrying of what would happen under the hood, use "Rename-DbaDatabase .... -Preview | Select-Object *" + + .PARAMETER SqlInstance + Target any number of instances, in order to return their build state. + + .PARAMETER SqlCredential + When connecting to an instance, use the credentials specified. + + .PARAMETER Database + Targets only specified databases + + .PARAMETER ExcludeDatabase + Excludes only specified databases + + .PARAMETER AllDatabases + If you want to apply the naming convention system wide, you need to pass this parameter + + .PARAMETER DatabaseName + Pass a template to rename the database name. Valid placeholders are: + - current database name + - date (yyyyMMdd) + + .PARAMETER FileGroupName + Pass a template to rename file group name. Valid placeholders are: + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER LogicalName + Pass a template to rename logical name. Valid placeholders are: + - file type (ROWS, LOG) + - current logical name + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER FileName + Pass a template to rename file name. Valid placeholders are: + - current file name (the basename, without directory nor extension) + - file type (ROWS, LOG, MMO, FS) + - current logical name + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER ReplaceBefore + If you pass this switch, all upper level "current names" will be inspected and replaced BEFORE doing the + rename according to the template in the current level (remember the hierarchy): + Let's say you have a database named "dbatools_HR", composed by 3 files + - dbatools_HR_Data.mdf + - dbatools_HR_Index.ndf + - dbatools_HR_log.ldf + Rename-DbaDatabase .... -Database "dbatools_HR" -DatabaseName "dbatools_HRARCHIVE" -FileName '' + would end up with this logic: + - database --> no placeholders specified + - dbatools_HR to dbatools_HRARCHIVE + - filenames placeholders specified + --> current database name + current filename" + - dbatools_HR_Data.mdf to dbatools_HRARCHIVEdbatools_HR_Data.mdf + - dbatools_HR_Index.mdf to dbatools_HRARCHIVEdbatools_HR_Data.mdf + - dbatools_HR_log.ldf to dbatools_HRARCHIVEdbatools_HR_log.ldf + Passing this switch, instead, e.g. + Rename-DbaDatabase .... -Database "dbatools_HR" -DatabaseName "dbatools_HRARCHIVE" -FileName '' -ReplaceBefore + end up with this logic instead: + - database --> no placeholders specified + - dbatools_HR to dbatools_HRARCHIVE + - filenames placeholders specified, + , plus -ReplaceBefore --> current database name + replace OLD "upper level" names inside the current filename + - dbatools_HR_Data.mdf to dbatools_HRARCHIVE_Data.mdf + - dbatools_HR_Index.mdf to dbatools_HRARCHIVE_Data.mdf + - dbatools_HR_log.ldf to dbatools_HRARCHIVE_log.ldf + + .PARAMETER Force + Kills any open session to be able to do renames. + + .PARAMETER SetOffline + Kills any open session and sets the database offline to be able to move files + + .PARAMETER Move + If you want this function to move files, else you're the one in charge of it. + This enables the same functionality as SetOffline, killing open transactions and putting the database + offline, then do the actual rename and setting it online again afterwards + + .PARAMETER Preview + Shows the renames without performing any operation (recommended to find your way around this function parameters ;-) ) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER InputObject + Accepts piped database objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Rename + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Rename-DbaDatabase + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName HR2 | select * + + Shows the detailed resultset you'll get renaming the HR database to HR2 without doing anything + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName HR2 + + Renames the HR database to HR2 + + .EXAMPLE + Get-DbaDatabase -SqlInstance sqlserver2014a -Database HR | Rename-DbaDatabase -DatabaseName HR2 + + Same as before, but with a piped database (renames the HR database to HR2) + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" + + Renames the HR database to dbatools_HR + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools__" + + Renames the HR database to dbatools_HR_20170807 (if today is 07th Aug 2017) + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -FileGroupName "dbatools_" + + Renames every FileGroup within HR to "dbatools_[the original FileGroup name]" + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileGroupName "_" + + Renames the HR database to "dbatools_HR", then renames every FileGroup within to "dbatools_HR_[the original FileGroup name]" + Note the "default recursive behaviour" here: for all intents and purposes the result of the former can be obtained with two distinct calls: + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -FileGroupName "dbatools__" + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db stays online (watch out!). You can then proceed manually to move/copy files by hand, set the db offline and then online again to finish the rename process + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" -SetOffline + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db is then set offline (watch out!). You can then proceed manually to move/copy files by hand and then set it online again to finish the rename process + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" -Move + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db is then set offline (watch out!). The function tries to do a simple rename and then sets the db online again to finish the rename process + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory, ParameterSetName = "Server")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [parameter(ParameterSetName = "Server")] + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [string]$DatabaseName, + [string]$FileGroupName, + [string]$LogicalName, + [string]$FileName, + [switch]$ReplaceBefore, + [switch]$Force, + [switch]$Move, + [switch]$SetOffline, + [switch]$Preview, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Pipe")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $CurrentDate = Get-Date -Format 'yyyyMMdd' + + function Get-DbaNameStructure($database) { + $obj = @() + # db name + $obj += "- Database : $database" + # FileGroups + foreach ($fg in $database.FileGroups) { + $obj += " - FileGroup: $($fg.Name)" + # LogicalNames + foreach ($ln in $fg.Files) { + $obj += " - Logical: $($ln.Name)" + $obj += " - FileName: $($ln.FileName)" + } + } + $obj += " - Logfiles" + foreach ($log in $database.LogFiles) { + $obj += " - Logical: $($log.Name)" + $obj += " - FileName: $($log.FileName)" + } + return $obj -Join "`n" + } + + + function Get-DbaKeyByValue($hashtable, $Value) { + ($hashtable.GetEnumerator() | Where-Object Value -eq $Value).Name + } + + if ((Test-Bound -ParameterName SetOffline) -and (-not(Test-Bound -ParameterName FileName))) { + Stop-Function -Category InvalidArgument -Message "-SetOffline is only useful when -FileName is passed. Quitting." + } + } + process { + if (Test-FunctionInterrupt) { return } + if (!$Database -and !$AllDatabases -and !$InputObject -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify a -AllDatabases or -Database/ExcludeDatabase to continue" + return + } + if (!$DatabaseName -and !$FileGroupName -and !$LogicalName -and !$FileName) { + Stop-Function -Message "You must specify at least one of -DatabaseName,-FileGroupName,-LogicalName or -Filename to continue" + return + } + $dbs = @() + if ($InputObject) { + if ($InputObject.Name) { + # comes from Get-DbaDatabase + $dbs += $InputObject + } + } + else { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $all_dbs = $server.Databases | Where-Object IsAccessible + $dbs += $all_dbs | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + } + + # holds all dbs per instance to avoid naming clashes + $InstanceDbs = @{} + + # holds all db file enumerations (used for -Move only) + $InstanceFiles = @{} + + #region db loop + foreach ($db in $dbs) { + # used to stop futher operations on database + $failed = $false + + # pending renames initialized at db level + $Pending_Renames = @() + + $Entities_Before = @{} + + $server = $db.Parent + if ($db.Name -in @('master', 'model', 'msdb', 'tempdb', 'distribution')) { + Write-Message -Level Warning -Message "Database $($db.Name) is a system one, skipping..." + continue + } + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $($db.Name) is not accessible, skipping..." + continue + } + if ($db.IsMirroringEnabled -eq $true -or $db.AvailabilityGroupName.Length -gt 0) { + Write-Message -Level Warning -Message "Database $($db.Name) is either mirrored or in an AG, skipping..." + continue + } + $Server_Id = $server.DomainInstanceName + if ( !$InstanceDbs.ContainsKey($Server_Id) ) { + $InstanceDbs[$Server_Id] = @{} + foreach ($dn in $server.Databases.Name) { + $InstanceDbs[$Server_Id][$dn] = 1 + } + } + + $Entities_Before['DBN'] = @{} + $Entities_Before['FGN'] = @{} + $Entities_Before['LGN'] = @{} + $Entities_Before['FNN'] = @{} + $Entities_Before['DBN'][$db.Name] = $db.Name + #region databasename + if ($DatabaseName) { + $Orig_DBName = $db.Name + # fixed replacements + $NewDBName = $DatabaseName.Replace('', $Orig_DBName).Replace('', $CurrentDate) + if ($Orig_DBName -eq $NewDBName) { + Write-Message -Level VeryVerbose -Message "Database name unchanged, skipping" + } + else { + if ($InstanceDbs[$Server_Id].ContainsKey($NewDBName)) { + Write-Message -Level Warning -Message "Database $NewDBName exists already, skipping this rename" + $failed = $true + } + else { + if ($PSCmdlet.ShouldProcess($db, "Renaming Database $db to $NewDBName")) { + if ($Force) { + $server.KillAllProcesses($Orig_DBName) + } + try { + if (!$Preview) { + $db.Rename($NewDBName) + } + $InstanceDbs[$Server_Id].Remove($Orig_DBName) + $InstanceDbs[$Server_Id][$NewDBName] = 1 + $Entities_Before['DBN'][$Orig_DBName] = $NewDBName + #$db.Refresh() + } + catch { + Stop-Function -Message "Failed to rename Database : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + } + } + } + } + } + #endregion databasename + #region filegroupname + if ($ReplaceBefore) { + #backfill PRIMARY + $Entities_Before['FGN']['PRIMARY'] = 'PRIMARY' + foreach ($fg in $db.FileGroups.Name) { + $Entities_Before['FGN'][$fg] = $fg + } + } + + if (!$failed -and $FileGroupName) { + $Editable_FGs = $db.FileGroups | Where-Object Name -ne 'PRIMARY' + $New_FGNames = @{} + foreach ($fg in $db.FileGroups.Name) { + $New_FGNames[$fg] = 1 + } + $FGCounter = 0 + foreach ($fg in $Editable_FGs) { + $Orig_FGName = $fg.Name + $Orig_Placeholder = $Orig_FGName + if ($ReplaceBefore) { + # at Filegroup level, we need to worry about database name + $Orig_Placeholder = $Orig_Placeholder.Replace($Entities_Before['DBN'][$Orig_DBName], '') + } + $NewFGName = $FileGroupName.Replace('', $Entities_Before['DBN'][$db.Name]).Replace('', $CurrentDate).Replace('', $Orig_Placeholder) + $FinalFGName = $NewFGName + while ($fg.Name -ne $FinalFGName) { + if ($FinalFGName -in $New_FGNames.Keys) { + $FGCounter += 1 + $FinalFGName = "$NewFGName$($FGCounter.ToString('000'))" + } + else { + break + } + } + if ($fg.Name -eq $FinalFGName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for FileGroup $($fg.Name) (on $db)" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming FileGroup $($fg.Name) to $FinalFGName")) { + try { + if (!$Preview) { + $fg.Rename($FinalFGName) + } + $New_FGNames.Remove($Orig_FGName) + $New_FGNames[$FinalFGName] = 1 + $Entities_Before['FGN'][$Orig_FGName] = $FinalFGName + } + catch { + Stop-Function -Message "Failed to rename FileGroup : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + #$db.FileGroups.Refresh() + } + + #endregion filegroupname + #region logicalname + if ($ReplaceBefore) { + foreach ($fn in $db.FileGroups.Files.Name) { + $Entities_Before['LGN'][$fn] = $fn + } + foreach ($fn in $db.Logfiles.Name) { + $Entities_Before['LGN'][$fn] = $fn + } + } + if (!$failed -and $LogicalName) { + $New_LogicalNames = @{} + foreach ($fn in $db.FileGroups.Files.Name) { + $New_LogicalNames[$fn] = 1 + } + foreach ($fn in $db.Logfiles.Name) { + $New_LogicalNames[$fn] = 1 + } + $LNCounter = 0 + foreach ($fg in $db.FileGroups) { + $logicalfiles = @($fg.Files) + for ($i = 0; $i -lt $logicalfiles.Count; $i++) { + $logical = $logicalfiles[$i] + $FileType = switch ($fg.FileGroupType) { + 'RowsFileGroup' { 'ROWS' } + 'MemoryOptimizedDataFileGroup' { 'MMO' } + 'FileStreamDataFileGroup' { 'FS' } + default { 'STD' } + } + $Orig_LGName = $logical.Name + $Orig_Placeholder = $Orig_LGName + if ($ReplaceBefore) { + # at Logical Name level, we need to worry about database name and filegroup name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '') + } + $NewLGName = $LogicalName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', $fg.Name).Replace( + '', $FileType).Replace('', $Orig_Placeholder) + $FinalLGName = $NewLGName + while ($logical.Name -ne $FinalLGName) { + if ($FinalLGName -in $New_LogicalNames.Keys) { + $LNCounter += 1 + $FinalLGName = "$NewLGName$($LNCounter.ToString('000'))" + } + else { + break + } + } + if ($logical.Name -eq $FinalLGName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for LogicalFile $($logical.Name) (on FileGroup $($fg.Name) (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming LogicalFile $($logical.Name) to $FinalLGName (on FileGroup $($fg.Name))")) { + try { + if (!$Preview) { + $logical.Rename($FinalLGName) + } + $New_LogicalNames.Remove($Orig_LGName) + $New_LogicalNames[$FinalLGName] = 1 + $Entities_Before['LGN'][$Orig_LGName] = $FinalLGName + } + catch { + Stop-Function -Message "Failed to Rename Logical File : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + } + #$fg.Files.Refresh() + if (!$failed) { + $logfiles = @($db.LogFiles) + for ($i = 0; $i -lt $logfiles.Count; $i++) { + $logicallog = $logfiles[$i] + $Orig_LGName = $logicallog.Name + $Orig_Placeholder = $Orig_LGName + if ($ReplaceBefore) { + # at Logical Name level, we need to worry about database name and filegroup name, but for logfiles filegroup is not there + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '') + } + $NewLGName = $LogicalName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', '').Replace( + '', 'LOG').Replace('', $Orig_Placeholder) + $FinalLGName = $NewLGName + if ($FinalLGName.Length -eq 0) { + #someone passed in -LogicalName ''.... but we don't have FGN here + $FinalLGName = $Orig_LGName + } + while ($logicallog.Name -ne $FinalLGName) { + if ($FinalLGName -in $New_LogicalNames.Keys) { + $LNCounter += 1 + $FinalLGName = "$NewLGName$($LNCounter.ToString('000'))" + } + else { + break + } + } + if ($logicallog.Name -eq $FinalLGName) { + Write-Message -Level VeryVerbose -Message "No Rename necessary for LogicalFile log $($logicallog.Name) (LOG on (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming LogicalFile log $($logicallog.Name) to $FinalLGName (LOG)")) { + try { + if (!$Preview) { + $logicallog.Rename($FinalLGName) + } + $New_LogicalNames.Remove($Orig_LGName) + $New_LogicalNames[$FinalLGName] = 1 + $Entities_Before['LGN'][$Orig_LGName] = $FinalLGName + } + catch { + Stop-Function -Message "Failed to Rename Logical File : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + #$db.Logfiles.Refresh() + } + } + #endregion logicalname + #region filename + if ($ReplaceBefore) { + foreach ($fn in $db.FileGroups.Files.FileName) { + $Entities_Before['FNN'][$fn] = $fn + } + foreach ($fn in $db.Logfiles.FileName) { + $Entities_Before['FNN'][$fn] = $fn + } + } + if (!$failed -and $FileName) { + + $New_FileNames = @{} + foreach ($fn in $db.FileGroups.Files.FileName) { + $New_FileNames[$fn] = 1 + } + foreach ($fn in $db.Logfiles.FileName) { + $New_FileNames[$fn] = 1 + } + # we need to inspect what files are in the same directory + # to avoid failing the process because the move won't work + # here we have a dict keyed by instance and then keyed by path + if ( !$InstanceFiles.ContainsKey($Server_Id) ) { + $InstanceFiles[$Server_Id] = @{} + } + foreach ($fn in $New_FileNames.Keys) { + $dirname = [IO.Path]::GetDirectoryName($fn) + if ( !$InstanceFiles[$Server_Id].ContainsKey($dirname) ) { + $InstanceFiles[$Server_Id][$dirname] = @{} + try { + $dirfiles = Get-DbaFile -SqlInstance $server -Path $dirname -EnableException + } + catch { + Write-Message -Level Warning -Message "Failed to enumerate existing files at $dirname, move could go wrong" + } + foreach ($f in $dirfiles) { + $InstanceFiles[$Server_Id][$dirname][$f.Filename] = 1 + } + } + } + $FNCounter = 0 + foreach ($fg in $db.FileGroups) { + $FG_Files = @($fg.Files) + foreach ($logical in $FG_Files) { + $FileType = switch ($fg.FileGroupType) { + 'RowsFileGroup' { 'ROWS' } + 'MemoryOptimizedDataFileGroup' { 'MMO' } + 'FileStreamDataFileGroup' { 'FS' } + default { 'STD' } + } + $FNName = $logical.FileName + $FNNameDir = [IO.Path]::GetDirectoryName($FNName) + $Orig_FNNameLeaf = [IO.Path]::GetFileNameWithoutExtension($logical.FileName) + $Orig_Placeholder = $Orig_FNNameLeaf + if ($ReplaceBefore) { + # at Filename level, we need to worry about database name, filegroup name and logical file name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['LGN'] -Value $logical.Name), '') + } + $NewFNName = $FileName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', $fg.Name).Replace( + '', $FileType).Replace('', $logical.Name).Replace('', $Orig_Placeholder) + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$([IO.Path]::GetExtension($FNName))") + + while ($logical.FileName -ne $FinalFNName) { + if ($InstanceFiles[$Server_Id][$FNNameDir].ContainsKey($FinalFNName)) { + $FNCounter += 1 + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$($FNCounter.ToString('000'))$([IO.Path]::GetExtension($FNName))" + ) + } + else { + break + } + } + if ($logical.FileName -eq $FinalFNName) { + Write-Message -Level VeryVerbose -Message "No rename necessary (on FileGroup $($fg.Name) (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming FileName $($logical.FileName) to $FinalFNName (on FileGroup $($fg.Name))")) { + try { + if (!$Preview) { + $logical.FileName = $FinalFNName + $db.Alter() + } + $InstanceFiles[$Server_Id][$FNNameDir].Remove($FNName) + $InstanceFiles[$Server_Id][$FNNameDir][$FinalFNName] = 1 + $Entities_Before['FNN'][$FNName] = $FinalFNName + $Pending_Renames += [pscustomobject]@{ + Source = $FNName + Destination = $FinalFNName + } + } + catch { + Stop-Function -Message "Failed to Rename FileName : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + if (!$failed) { + $FG_Files = @($db.Logfiles) + foreach ($logical in $FG_Files) { + $FNName = $logical.FileName + $FNNameDir = [IO.Path]::GetDirectoryName($FNName) + $Orig_FNNameLeaf = [IO.Path]::GetFileNameWithoutExtension($logical.FileName) + $Orig_Placeholder = $Orig_FNNameLeaf + if ($ReplaceBefore) { + # at Filename level, we need to worry about database name, filegroup name and logical file name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['LGN'] -Value $logical.Name), '') + } + $NewFNName = $FileName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', '').Replace( + '', 'LOG').Replace('', $logical.Name).Replace('', $Orig_Placeholder) + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$([IO.Path]::GetExtension($FNName))") + while ($logical.FileName -ne $FinalFNName) { + if ($InstanceFiles[$Server_Id][$FNNameDir].ContainsKey($FinalFNName)) { + $FNCounter += 1 + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$($FNCounter.ToString('000'))$([IO.Path]::GetExtension($FNName))") + } + else { + break + } + } + if ($logical.FileName -eq $FinalFNName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for $($logical.FileName) (LOG on (on $db))" + continue + } + + if ($PSCmdlet.ShouldProcess($db, "Renaming FileName $($logical.FileName) to $FinalFNName (LOG)")) { + try { + if (!$Preview) { + $logical.FileName = $FinalFNName + $db.Alter() + } + $InstanceFiles[$Server_Id][$FNNameDir].Remove($FNName) + $InstanceFiles[$Server_Id][$FNNameDir][$FinalFNName] = 1 + $Entities_Before['FNN'][$FNName] = $FinalFNName + $Pending_Renames += [pscustomobject]@{ + Source = $FNName + Destination = $FinalFNName + } + } + catch { + Stop-Function -Message "Failed to Rename FileName : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + } + + } + #endregion filename + #region move + $ComputerName = $null + $Final_Renames = New-Object System.Collections.ArrayList + if ([DbaValidate]::IsLocalhost($server.NetName)) { + # locally ran so we can just use rename-item + $ComputerName = $server.NetName + } + else { + # let's start checking if we can access .NetName + $testPS = $false + if ($SqlCredential) { + # why does Test-PSRemoting require a Credential param ? this is ugly... + $testPS = Test-PSRemoting -ComputerName $server.NetName -Credential $SqlCredential -ErrorAction Stop + } + else { + $testPS = Test-PSRemoting -ComputerName $server.NetName -ErrorAction Stop + } + if (!($testPS)) { + # let's try to resolve it to a more qualified name, without "cutting" knowledge about the domain (only $server.Name possibly holds the complete info) + $Resolved = (Resolve-DbaNetworkName -ComputerName $server.Name).FullComputerName + if ($SqlCredential) { + $testPS = Test-PSRemoting -ComputerName $Resolved -Credential $SqlCredential -ErrorAction Stop + } + else { + $testPS = Test-PSRemoting -ComputerName $Resolved -ErrorAction Stop + } + if ($testPS) { + $ComputerName = $Resolved + } + } + else { + $ComputerName = $server.NetName + } + } + foreach ($op in $pending_renames) { + if ([DbaValidate]::IsLocalhost($server.NetName)) { + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + else { + if ($null -eq $ComputerName) { + # if we don't have remote access ($ComputerName is null) we can fallback to admin shares if they're available + if (Test-Path (Join-AdminUnc -ServerName $server.NetName -filepath $op.Source)) { + $null = $Final_Renames.Add([pscustomobject]@{ + Source = Join-AdminUnc -ServerName $server.NetName -filepath $op.Source + Destination = Join-AdminUnc -ServerName $server.NetName -filepath $op.Destination + ComputerName = $server.NetName + }) + } + else { + # flag the impossible rename ($ComputerName is $null) + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + } + else { + # we can do renames in a remote pssession + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + } + } + $Status = 'FULL' + if (!$failed -and ($SetOffline -or $Move) -and $Final_Renames) { + if (!$Move) { + Write-Message -Level VeryVerbose -Message "Setting the database offline. You are in charge of moving the files to the new location" + # because renames still need to be dealt with + $Status = 'PARTIAL' + } + else { + if ($PSCmdlet.ShouldProcess($db, "File Rename required, setting db offline")) { + $SetState = Set-DbaDatabaseState -SqlInstance $server -Database $db.Name -Offline -Force + if ($SetState.Status -ne 'OFFLINE') { + Write-Message -Level Warning -Message "Setting db offline failed, You are in charge of moving the files to the new location" + # because it was impossible to set the database offline + $Status = 'PARTIAL' + } + else { + try { + while ($Final_Renames.Count -gt 0) { + $op = $Final_Renames.Item(0) + if ($null -eq $op.ComputerName) { + Stop-Function -Message "No access to physical files for renames" + } + else { + Write-Message -Level VeryVerbose -Message "Moving file $($op.Source) to $($op.Destination)" + if (!$Preview) { + $scriptblock = { + $op = $args[0] + Rename-Item -Path $op.Source -NewName $op.Destination + } + Invoke-Command2 -ComputerName $op.ComputerName -Credential $sqlCredential -ScriptBlock $scriptblock -ArgumentList $op + } + } + $null = $Final_Renames.RemoveAt(0) + } + } + catch { + $failed = $true + # because a rename operation failed + $Status = 'PARTIAL' + Stop-Function -Message "Failed to rename $($op.Source) to $($op.Destination), you are in charge of moving the files to the new location" -ErrorRecord $_ -Target $instance -Exception $_.Exception -Continue + } + if (!$failed) { + if ($PSCmdlet.ShouldProcess($db, "Setting database online")) { + $SetState = Set-DbaDatabaseState -SqlInstance $server -Database $db.Name -Online -Force + if ($SetState.Status -ne 'ONLINE') { + Write-Message -Level Warning -Message "Setting db online failed" + # because renames were done, but the database didn't wake up + $Status = 'PARTIAL' + } + else { + $Status = 'FULL' + } + } + } + } + } + } + } + else { + # because of a previous error with renames to do + $Status = 'PARTIAL' + } + } + else { + if (!$failed) { + # because no previous error and not filename + $Status = 'FULL' + } + else { + # because previous errors and not filename + $Status = 'PARTIAL' + } + } + #endregion move + # remove entities that match for the output + foreach ($k in $Entities_Before.Keys) { + $ToRemove = $Entities_Before[$k].GetEnumerator() | Where-Object { $_.Name -eq $_.Value } | Select-Object -ExpandProperty Name + foreach ($el in $ToRemove) { + $Entities_Before[$k].Remove($el) + } + } + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + DBN = $Entities_Before['DBN'] + DatabaseRenames = ($Entities_Before['DBN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + FGN = $Entities_Before['FGN'] + FileGroupsRenames = ($Entities_Before['FGN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + LGN = $Entities_Before['LGN'] + LogicalNameRenames = ($Entities_Before['LGN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + FNN = $Entities_Before['FNN'] + FileNameRenames = ($Entities_Before['FNN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + PendingRenames = $Final_Renames + Status = $Status + } | Select-DefaultView -ExcludeProperty DatabaseRenames, FileGroupsRenames, LogicalNameRenames, FileNameRenames + } + #endregion db loop + } +} +function Rename-DbaLogin { + <# +.SYNOPSIS +Rename-DbaLogin will rename login and database mapping for a specified login. + +.DESCRIPTION +There are times where you might want to rename a login that was copied down, or if the name is not descriptive for what it does. + +It can be a pain to update all of the mappings for a specific user, this does it for you. + +.PARAMETER SqlInstance +Source SQL Server.You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER Destination +Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Login +The current Login on the server - this list is auto-populated from the server. + +.PARAMETER NewLogin +The new Login that you wish to use. If it is a windows user login, then the SID must match. + +.PARAMETER Confirm +Prompts to confirm actions + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER EnableException +By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. +This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. +Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + +.NOTES +Tags: Login +Author: Mitchell Hamann (@SirCaptainMitch) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Rename-DbaLogin + +.EXAMPLE +Rename-DbaLogin -SqlInstance localhost -Login DbaToolsUser -NewLogin captain + +SQL Login Example + +.EXAMPLE +Rename-DbaLogin -SqlInstance localhost -Login domain\oldname -NewLogin domain\newname + +Change the windowsuser login name. + +.EXAMPLE +Rename-DbaLogin -SqlInstance localhost -Login dbatoolsuser -NewLogin captain -WhatIf + +WhatIf Example +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [string]$Login, + [parameter(Mandatory = $true)] + [string]$NewLogin, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $Databases = $server.Databases | Where-Object IsAccessible + $currentLogin = $server.Logins[$Login] + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing Login name from [$Login] to [$NewLogin]")) { + try { + $dbenums = $currentLogin.EnumDatabaseMappings() + $currentLogin.rename($NewLogin) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $null + OldLogin = $Login + NewLogin = $NewLogin + Status = "Successful" + } + } + catch { + $dbenums = $null + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $null + OldLogin = $Login + NewLogin = $NewLogin + Status = "Failure" + } + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $login + } + } + + foreach ($db in $dbenums) { + $db = $databases[$db.DBName] + $user = $db.Users[$Login] + Write-Message -Level Verbose -Message "Starting update for $db" + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing database $db user $user from [$Login] to [$NewLogin]")) { + try { + $oldname = $user.name + $user.Rename($NewLogin) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OldUser = $oldname + NewUser = $NewLogin + Status = "Successful" + } + + } + catch { + Write-Message -Level Warning -Message "Rolling back update to login: $Login" + $currentLogin.rename($Login) + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OldUser = $NewLogin + NewUser = $oldname + Status = "Failure to rename. Rolled back change." + } + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $NewLogin + } + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Repair-DbaOrphanUser { + <# + .SYNOPSIS + Finds orphan users with existing login and remaps them. + + .DESCRIPTION + An orphan user is defined by a user that does not have a matching login (Login property = ""). + + If the matching login exists it must be: + Enabled + Not a system object + Not locked + Have the same name that user + + You can drop users that does not have their matching login by specifying the parameter -RemoveNotExisting. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER Users + Specifies the list of usernames to repair. + + .PARAMETER Force + Forces alter schema to dbo owner so users can be dropped. + + .PARAMETER RemoveNotExisting + If this switch is enabled, all users that do not have a matching login will be dropped from the database. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sql2005 + + Finds and repairs all orphan users of all databases present on server 'sql2005' + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -SqlCredential $cred + + Finds and repair all orphan users in all databases present on server 'sqlserver2014a'. SQL credentials are used to authenticate to the server. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1, db2 + + Finds and repairs all orphan users in both db1 and db2 databases. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1 -Users OrphanUser + + Finds and repairs user 'OrphanUser' in 'db1' database. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Users OrphanUser + + Finds and repairs user 'OrphanUser' on all databases + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -RemoveNotExisting + + Finds all orphan users of all databases present on server 'sqlserver2014a'. Removes all users that do not have matching Logins. + + .NOTES + Tags: Orphan + Author: Claudio Silva (@ClaudioESSilva) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Repair-DbaOrphanUser + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [object[]]$Users, + [switch]$RemoveNotExisting, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $SqlInstance." + continue + } + + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($DatabaseCollection.Count -gt 0) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + + Write-Message -Level Verbose -Message "Validating users on database '$db'." + + if ($Users.Count -eq 0) { + #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false } + } + else { + + #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($Users -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + + } + + if ($UsersToWork.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found" + $UsersToRemove = @() + foreach ($User in $UsersToWork) { + $ExistLogin = $server.logins | Where-Object { + $_.Isdisabled -eq $False -and + $_.IsSystemObject -eq $False -and + $_.IsLocked -eq $False -and + $_.Name -eq $User.Name + } + + if ($ExistLogin) { + if ($server.versionMajor -gt 8) { + $query = "ALTER USER " + $User + " WITH LOGIN = " + $User + } + else { + $query = "exec sp_change_users_login 'update_one', '$User'" + } + + if ($Pscmdlet.ShouldProcess($db.Name, "Mapping user '$($User.Name)'")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User '$($User.Name)' mapped with their login." + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $User.Name + Status = "Success" + } + } + } + else { + if ($RemoveNotExisting) { + #add user to collection + $UsersToRemove += $User + } + else { + Write-Message -Level Verbose -Message "Orphan user $($User.Name) does not have matching login." + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $User.Name + Status = "No matching login" + } + } + } + } + + #With the collection complete invoke remove. + if ($RemoveNotExisting) { + if ($Force) { + if ($Pscmdlet.ShouldProcess($db.Name, "Remove-DbaOrphanUser")) { + Write-Message -Level Verbose -Message "Calling 'Remove-DbaOrphanUser' with -Force." + Remove-DbaOrphanUser -SqlInstance $server -Database $db.Name -User $UsersToRemove -Force + } + } + else { + if ($Pscmdlet.ShouldProcess($db.Name, "Remove-DbaOrphanUser")) { + Write-Message -Level Verbose -Message "Calling 'Remove-DbaOrphanUser'." + Remove-DbaOrphanUser -SqlInstance $server -Database $db.Name -User $UsersToRemove + } + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database '$db'." + } + #reset collection + $UsersToWork = $null + } + catch { + Stop-Function -Message $_ -Continue + } + } + } + else { + Write-Message -Level Verbose -Message "There are no databases to analyse." + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Repair-SqlOrphanUser + } +} +function Repair-DbaServerName { + <# + .SYNOPSIS + Renames @@SERVERNAME to match with the Windows name. + + .DESCRIPTION + When a SQL Server's host OS is renamed, the SQL Server should be as well. This helps with Availability Groups and Kerberos. + + This command renames @@SERVERNAME to match with the Windows name. The new name is automatically determined. It does not matter if you use an alias to connect to the SQL instance. + + If the automatically determined new name matches the old name, the command will not run. + + https://www.mssqltips.com/sqlservertip/2525/steps-to-change-the-server-name-for-a-sql-server-machine/ + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER AutoFix + If this switch is enabled, the repair will be performed automatically. + + .PARAMETER Force + If this switch is enabled, most confirmation prompts will be skipped. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: SPN + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Repair-DbaServerName + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 + + Checks to see if the server name is updatable and changes the name with a number of prompts. + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 -AutoFix + + Checks to see if the server name is updatable and automatically performs the change. Replication or mirroring will be broken if necessary. + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 -AutoFix -Force + + Checks to see if the server name is updatable and automatically performs the change, bypassing most prompts and confirmations. Replication or mirroring will be broken if necessary. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [switch]$AutoFix, + [switch]$Force, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($Force -eq $true) { + $ConfirmPreference = "None" + } + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.isClustered) { + Write-Message -Level Warning -Message "$instance is a cluster. Microsoft does not support renaming clusters." + continue + } + + + # Check to see if we can easily proceed + + $nametest = Test-DbaServerName $server -EnableException | Select-Object * + $oldserverinstancename = $nametest.ServerName + $SqlInstancename = $nametest.SqlInstance + + if ($nametest.RenameRequired -eq $false) { + Stop-Function -Continue -Message "Good news! $oldserverinstancename's @@SERVERNAME does not need to be changed. If you'd like to rename it, first rename the Windows server." + } + + if (-not $nametest.updatable) { + Write-Message -Level Output -Message "Test-DbaServerName reports that the rename cannot proceed with a rename in this $instance's current state." + + foreach ($nametesterror in $nametest.Blockers) { + if ($nametesterror -like '*replication*') { + + if (-not $AutoFix) { + Stop-Function -Message "Cannot proceed because some databases are involved in replication. You can run exec sp_dropdistributor @no_checks = 1 but that may be pretty dangerous. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also break all database mirrors." + return + } + else { + if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) { + $title = "You have chosen to AutoFix the blocker: replication." + $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 1) + + if ($result -eq 1) { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + else { + Write-Message -Level Output -Message "`nPerforming sp_dropdistributor @no_checks = 1." + $sql = "sp_dropdistributor @no_checks = 1" + Write-Message -Level Debug -Message $sql + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + } + } + } + } + elseif ($Error -like '*mirror*') { + if ($AutoFix -eq $false) { + Stop-Function -Message "Cannot proceed because some databases are being mirrored. Stop mirroring to proceed. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also stop replication." -Continue + } + else { + if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) { + $title = "You have chosen to AutoFix the blocker: mirroring." + $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 1) + + if ($result -eq 1) { + Write-Message -Level Output -Message "Okay, moving on." + } + else { + Write-Message -Level Verbose -Message "Removing Mirroring" + + foreach ($database in $server.Databases) { + if ($database.IsMirroringEnabled) { + $dbname = $database.name + + try { + Write-Message -Level Verbose -Message "Breaking mirror for $dbname." + $database.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $database.Alter() + $database.Refresh() + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + #throw "Could not break mirror for $dbname. Skipping." + } + } + } + } + } + } + } + } + } + # ^ That's embarrassing + + $instancename = $server.InstanceName + + if (-not $instancename) { + $instancename = "MSSQLSERVER" + } + + try { + $allsqlservices = Get-Service -ComputerName $instance.ComputerName -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "SQL*$instancename*" -and $_.Status -eq "Running" } + } + catch { + Write-Message -Level Warning -Message "Can't contact $instance using Get-Service. This means the script will not be able to automatically restart SQL services." + } + + if ($nametest.Warnings.length -gt 0) { + $reportingservice = Get-Service -ComputerName $instance.ComputerName -DisplayName "SQL Server Reporting Services ($instancename)" -ErrorAction SilentlyContinue + + if ($reportingservice.Status -eq "Running") { + if ($Pscmdlet.ShouldProcess($server.name, "Reporting Services is running for this instance. Would you like to automatically stop this service?")) { + $reportingservice | Stop-Service + Write-Message -Level Warning -Message "You must reconfigure Reporting Services using Reporting Services Configuration Manager or PowerShell once the server has been successfully renamed." + } + } + } + + if ($Pscmdlet.ShouldProcess($server.name, "Performing sp_dropserver to remove the old server name, $oldserverinstancename, then sp_addserver to add $SqlInstancename")) { + $sql = "sp_dropserver '$oldserverinstancename'" + Write-Message -Level Debug -Message $sql + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + } + + $sql = "sp_addserver '$SqlInstancename', local" + Write-Message -Level Debug -Message $sql + + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + } + $renamed = $true + } + + if ($null -eq $allsqlservices) { + Write-Message -Level Warning -Message "Could not contact $($instance.ComputerName) using Get-Service. You must manually restart the SQL Server instance." + $needsrestart = $true + } + else { + if ($Pscmdlet.ShouldProcess($instance.ComputerName, "Rename complete! The SQL Service must be restarted to commit the changes. Would you like to restart the $instancename instance now?")) { + try { + Write-Message -Level Verbose -Message "Stopping SQL Services for the $instancename instance" + $allsqlservices | Stop-Service -Force -WarningAction SilentlyContinue # because it reports the wrong name + Write-Message -Level Verbose -Message "Starting SQL Services for the $instancename instance." + $allsqlservices | Where-Object { $_.DisplayName -notlike "*reporting*" } | Start-Service -WarningAction SilentlyContinue # because it reports the wrong name + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + } + } + + if ($renamed -eq $true) { + Write-Message -Level Verbose -Message "$instance successfully renamed from $oldserverinstancename to $SqlInstancename." + Test-DbaServerName -SqlInstance $server + } + + if ($needsrestart -eq $true) { + Write-Message -Level Warning -Message "SQL Service restart for $SqlInstancename still required." + } + } + } +} +function Reset-DbaAdmin { + <# + .SYNOPSIS + This function allows administrators to regain access to SQL Servers in the event that passwords or access was lost. + + Supports SQL Server 2005 and above. Windows administrator access is required. + + .DESCRIPTION + This function allows administrators to regain access to local or remote SQL Servers by either resetting the sa password, adding the sysadmin role to existing login, or adding a new login (SQL or Windows) and granting it sysadmin privileges. + + This is accomplished by stopping the SQL services or SQL Clustered Resource Group, then restarting SQL via the command-line using the /mReset-DbaAdmin parameter which starts the server in Single-User mode and only allows this script to connect. + + Once the service is restarted, the following tasks are performed: + - Login is added if it doesn't exist + - If login is a Windows User, an attempt is made to ensure it exists + - If login is a SQL Login, password policy will be set to OFF when creating the login, and SQL Server authentication will be set to Mixed Mode. + - Login will be enabled and unlocked + - Login will be added to sysadmin role + + If failures occur at any point, a best attempt is made to restart the SQL Server. + + In order to make this script as portable as possible, System.Data.SqlClient and Get-WmiObject are used (as opposed to requiring the Failover Cluster Admin tools or SMO). + + If using this function against a remote SQL Server, ensure WinRM is configured and accessible. If this is not possible, run the script locally. + + Tested on Windows XP, 7, 8.1, Server 2012 and Windows Server Technical Preview 2. + Tested on SQL Server 2005 SP4 through 2016 CTP2. + + .PARAMETER SqlInstance + The SQL Server instance. SQL Server must be 2005 and above, and can be a clustered or stand-alone instance. + + .PARAMETER Login + By default, the Login parameter is "sa" but any other SQL or Windows account can be specified. If a login does not currently exist, it will be added. + + When adding a Windows login to remote servers, ensure the SQL Server can add the login (ie, don't add WORKSTATION\Admin to remoteserver\instance. Domain users and Groups are valid input. + + .PARAMETER SecurePassword + By default, if a SQL Login is detected, you will be prompted for a password. Use this to securely bypass the prompt. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Login(s) will be dropped and recreated on Destination. Logins that own Agent jobs cannot be dropped at this time. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlcluster + + Prompts for password, then resets the "sa" account password on sqlcluster. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlserver\sqlexpress -Login ad\administrator + + Prompts user to confirm that they understand the SQL Service will be restarted. + + Adds the domain account "ad\administrator" as a sysadmin to the SQL instance. + If the account already exists, it will be added to the sysadmin role. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlserver\sqlexpress -Login sqladmin -Force + + Skips restart confirmation, prompts for password, then adds a SQL Login "sqladmin" with sysadmin privileges. + If the account already exists, it will be added to the sysadmin role and the password will be reset. + + .NOTES + Tags: WSMan + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: Admin access to server (not SQL Services), + Remoting must be enabled and accessible if $SqlInstance is not local + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + + .LINK + https://dbatools.io/Reset-DbaAdmin +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter] + $SqlInstance, + [string]$Login = "sa", + [SecureString]$SecurePassword, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Reset-SqlAdmin + + #region Utility functions + function ConvertTo-PlainText { + <# + .SYNOPSIS + Internal function. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Security.SecureString] + $Password + ) + + $marshal = [Runtime.InteropServices.Marshal] + $plaintext = $marshal::PtrToStringAuto($marshal::SecureStringToBSTR($Password)) + return $plaintext + } + + function Invoke-ResetSqlCmd { + <# + .SYNOPSIS + Internal function. Executes a SQL statement against specified computer, and uses "Reset-DbaAdmin" as the Application Name. + #> + [OutputType([System.Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter] + $SqlInstance, + [string]$sql + ) + try { + $connstring = "Data Source=$SqlInstance;Integrated Security=True;Connect Timeout=2;Application Name=Reset-DbaAdmin" + $conn = New-Object System.Data.SqlClient.SqlConnection $connstring + $conn.Open() + $cmd = New-Object system.data.sqlclient.sqlcommand($null, $conn) + $cmd.CommandText = $sql + $cmd.ExecuteNonQuery() | Out-Null + $cmd.Dispose() + $conn.Close() + $conn.Dispose() + return $true + } + catch { + return $false + } + } + #endregion Utility functions + } + + process { + if ($Force) { + $ConfirmPreference = "none" + } + + $baseaddress = $SqlInstance.ComputerName + + # Before we continue, we need confirmation. + if ($pscmdlet.ShouldProcess($baseaddress, "Reset-DbaAdmin (SQL Server instance $SqlInstance will restart)")) { + # Get hostname + + if ($baseaddress -eq "." -or $baseaddress -eq $env:COMPUTERNAME -or $baseaddress -eq "localhost") { + $ipaddr = "." + $hostname = $env:COMPUTERNAME + $baseaddress = $env:COMPUTERNAME + } + + # If server is not local, get IP address and NetBios name in case CNAME records were referenced in the SQL hostname + if ($baseaddress -ne $env:COMPUTERNAME) { + # Test for WinRM #Test-WinRM neh + winrm id -r:$baseaddress 2>$null | Out-Null + if ($LastExitCode -ne 0) { + throw "Remote PowerShell access not enabled on on $source or access denied. Quitting." + } + + # Test Connection first using Test-Connection which requires ICMP access then failback to tcp if pings are blocked + Write-Message -Level Verbose -Message "Testing connection to $baseaddress" + $testconnect = Test-Connection -ComputerName $baseaddress -Count 1 -Quiet + + if ($testconnect -eq $false) { + Write-Message -Level Verbose -Message "First attempt using ICMP failed. Trying to connect using sockets. This may take up to 20 seconds." + $tcp = New-Object System.Net.Sockets.TcpClient + try { + $tcp.Connect($hostname, 135) + $tcp.Close() + $tcp.Dispose() + } + catch { + throw "Can't connect to $baseaddress either via ping or tcp (WMI port 135)" + } + } + Write-Message -Level Verbose -Message "Resolving IP address." + try { + $hostentry = [System.Net.Dns]::GetHostEntry($baseaddress) + $ipaddr = ($hostentry.AddressList | Where-Object { $_ -notlike '169.*' } | Select-Object -First 1).IPAddressToString + } + catch { + throw "Could not resolve SqlServer IP or NetBIOS name" + } + + Write-Message -Level Verbose -Message "Resolving NetBIOS name." + try { + $hostname = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=TRUE -ComputerName $ipaddr).PSComputerName + if ($null -eq $hostname) { + $hostname = (nbtstat -A $ipaddr | Where-Object { $_ -match '\<00\> UNIQUE' } | ForEach-Object { $_.SubString(4, 14) }).Trim() + } + } + catch { + throw "Could not access remote WMI object. Check permissions and firewall." + } + } + + # Setup remote session if server is not local + if ($hostname -ne $env:COMPUTERNAME) { + try { + $session = New-PSSession -ComputerName $hostname + } + catch { + throw "Can't access $hostname using PSSession. Check your firewall settings and ensure Remoting is enabled or run the script locally." + } + } + + Write-Message -Level Verbose -Message "Detecting login type." + # Is login a Windows login? If so, does it exist? + if ($login -match "\\") { + Write-Message -Level Verbose -Message "Windows login detected. Checking to ensure account is valid." + $windowslogin = $true + try { + if ($hostname -eq $env:COMPUTERNAME) { + $account = New-Object System.Security.Principal.NTAccount($args) + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + } + else { + Invoke-Command -ErrorAction Stop -Session $session -ArgumentList $login -ScriptBlock { + $account = New-Object System.Security.Principal.NTAccount($args) + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + } + } + } + catch { + Write-Message -Level Warning -Message "Cannot resolve Windows User or Group $login. Trying anyway." + } + } + + # If it's not a Windows login, it's a SQL login, so it needs a password. + if ($windowslogin -ne $true -and (Test-Bound -Not -ParameterName SecurePassword)) { + Write-Message -Level Verbose -Message "SQL login detected" + do { + $Password = Read-Host -AsSecureString "Please enter a new password for $login" + } + while ($Password.Length -eq 0) + } + + If ((Test-Bound -ParameterName SecurePassword)) { + $Password = $SecurePassword + } + + # Get instance and service display name, then get services + $instance = $null + $instance = $SqlInstance.InstanceName + if (-not $instance) { + $instance = "MSSQLSERVER" + } + $displayName = "SQL Server ($instance)" + + try { + if ($hostname -eq $env:COMPUTERNAME) { + $instanceservices = Get-Service -ErrorAction Stop | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } + $sqlservice = Get-Service -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instance)" + } + else { + $instanceservices = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } + $sqlservice = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instance)" + } + } + catch { + Stop-Function -Message "Cannot connect to WMI on $hostname or SQL Service does not exist. Check permissions, firewall and SQL Server running status." -ErrorRecord $_ -Target $SqlInstance + return + } + + if (-not $instanceservices) { + Stop-Function -Message "Couldn't find SQL Server instance. Check the spelling, ensure the service is running and try again." -Target $SqlInstance + return + } + + Write-Message -Level Verbose -Message "Attempting to stop SQL Services." + + # Check to see if service is clustered. Clusters don't support -m (since the cluster service + # itself connects immediately) or -f, so they are handled differently. + try { + $checkcluster = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object { $_.Name -eq "ClusSvc" -and $_.Status -eq "Running" } + } + catch { + Stop-Function -Message "Can't check services." -Target $SqlInstance -ErrorRecord $_ + return + } + + if ($null -ne $checkcluster) { + $clusterResource = Get-DbaCmObject -ClassName "MSCluster_Resource" -Namespace "root\mscluster" -ComputerName $hostname | + Where-Object { $_.Name.StartsWith("SQL Server") -and $_.OwnerGroup -eq "SQL Server ($instance)" } + } + + # Take SQL Server offline so that it can be started in single-user mode + if ($clusterResource.count -gt 0) { + $isclustered = $true + try { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.TakeOffline(60) } + } + catch { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL Service and quit." -ErrorRecord $_ -Target $SqlInstance + return + } + } + else { + try { + Stop-Service -InputObject $sqlservice -Force -ErrorAction Stop + Write-Message -Level Verbose -Message "Successfully stopped SQL service." + } + catch { + Start-Service -InputObject $instanceservices -ErrorAction Stop + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL service and quit." -ErrorRecord $_ -Target $SqlInstance + return + } + } + + # /mReset-DbaAdmin Starts an instance of SQL Server in single-user mode and only allows this script to connect. + Write-Message -Level Verbose -Message "Starting SQL Service from command line." + try { + if ($hostname -eq $env:COMPUTERNAME) { + $netstart = net start ""$displayname"" /mReset-DbaAdmin 2>&1 + if ("$netstart" -notmatch "success") { + throw + } + } + else { + $netstart = Invoke-Command -ErrorAction Stop -Session $session -ArgumentList $displayname -ScriptBlock { net start ""$args"" /mReset-DbaAdmin } 2>&1 + foreach ($line in $netstart) { + if ($line.length -gt 0) { Write-Message -Level Verbose -Message $line } + } + } + } + catch { + Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue + + if ($isclustered) { + $clusterResource | Where-Object Name -eq "SQL Server" | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object Name -ne "SQL Server" | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + Stop-Function -Message "Couldn't execute net start command. Restarted services and quit." -ErrorRecord $_ + return + } + + Write-Message -Level Verbose -Message "Reconnecting to SQL instance." + try { + $null = Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql "SELECT 1" -ErrorAction Stop + } + catch { + try { + Start-Sleep 3 + $null = Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql "SELECT 1" -ErrorAction Stop + } + catch { + Stop-Service Input-Object $sqlservice -Force -ErrorAction SilentlyContinue + if ($isclustered) { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL Service and quit." -ErrorRecord $_ + } + } + + # Get login. If it doesn't exist, create it. + Write-Message -Level Verbose -Message "Adding login $login if it doesn't exist." + if ($windowslogin -eq $true) { + $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') + BEGIN CREATE LOGIN [$login] FROM WINDOWS END" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't create login." + } + + } + elseif ($login -ne "sa") { + # Create new sql user + $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') + BEGIN CREATE LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)', CHECK_POLICY = OFF, CHECK_EXPIRATION = OFF END" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't create login." + } + } + + # If $login is a SQL Login, Mixed mode authentication is required. + if ($windowslogin -ne $true) { + Write-Message -Level Verbose -Message "Enabling mixed mode authentication." + Write-Message -Level Verbose -Message "Ensuring account is unlocked." + $sql = "EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'LoginMode', REG_DWORD, 2" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't set to Mixed Mode." + } + + $sql = "ALTER LOGIN [$login] WITH CHECK_POLICY = OFF + ALTER LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)' UNLOCK" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't unlock account." + } + } + + Write-Message -Level Verbose -Message "Ensuring login is enabled." + $sql = "ALTER LOGIN [$login] ENABLE" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't enable login." + } + + if ($login -ne "sa") { + Write-Message -Level Verbose -Message "Ensuring login exists within sysadmin role." + $sql = "EXEC sp_addsrvrolemember '$login', 'sysadmin'" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't add to sysadmin role." + } + } + + Write-Message -Level Verbose -Message "Finished with login tasks." + Write-Message -Level Verbose -Message "Restarting SQL Server." + Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue + if ($isclustered -eq $true) { + $clusterResource | Where-Object Name -eq "SQL Server" | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object Name -ne "SQL Server" | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + } + } + end { + Write-Message -Level Verbose -Message "Script complete!" + } +} +function Resolve-DbaNetworkName { + <# + .SYNOPSIS + Returns information about the network connection of the target computer including NetBIOS name, IP Address, domain name and fully qualified domain name (FQDN). + + .DESCRIPTION + Retrieves the IPAddress, ComputerName from one computer. + The object can be used to take action against its name or IPAddress. + + First ICMP is used to test the connection, and get the connected IPAddress. + + Multiple protocols (e.g. WMI, CIM, etc) are attempted before giving up. + + Important: Remember that FQDN doesn't always match "ComputerName dot Domain" as AD intends. + There are network setup (google "disjoint domain") where AD and DNS do not match. + "Full computer name" (as reported by sysdm.cpl) is the only match between the two, + and it matches the "DNSHostName" property of the computer object stored in AD. + This means that the notation of FQDN that matches "ComputerName dot Domain" is incorrect + in those scenarios. + In other words, the "suffix" of the FQDN CAN be different from the AD Domain. + + This cmdlet has been providing good results since its inception but for lack of useful + names some doubts may arise. + Let this clear the doubts: + - InputName: whatever has been passed in + - ComputerName: hostname only + - IPAddress: IP Address + - DNSHostName: hostname only, coming strictly from DNS (as reported from the calling computer) + - DNSDomain: domain only, coming strictly from DNS (as reported from the calling computer) + - Domain: domain only, coming strictly from AD (i.e. the domain the ComputerName is joined to) + - DNSHostEntry: Fully name as returned by DNS [System.Net.Dns]::GetHostEntry + - FQDN: "legacy" notation of ComputerName "dot" Domain (coming from AD) + - FullComputerName: Full name as configured from within the Computer (i.e. the only secure match between AD and DNS) + + So, if you need to use something, go with FullComputerName, always, as it is the most correct in every scenario. + + .PARAMETER ComputerName + The Server that you're connecting to. + This can be the name of a computer, a SMO object, an IP address or a SQL Instance. + + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Turbo + Resolves without accessing the server itself. Faster but may be less accurate because it relies on DNS only, + so it may fail spectacularly for disjoin-domain setups. Also, everyone has its own DNS (i.e. results may vary + changing the computer where the function runs) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Network, Resolve + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Editor: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Resolve-DbaNetworkName + + .EXAMPLE + Resolve-DbaNetworkName -ComputerName ServerA + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for ServerA + + .EXAMPLE + Resolve-DbaNetworkName -SqlInstance sql2016\sqlexpress + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for the SQL instance sql2016\sqlexpress + + .EXAMPLE + Resolve-DbaNetworkName -SqlInstance sql2016\sqlexpress, sql2014 + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for the SQL instance sql2016\sqlexpress and sql2014 + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Resolve-DbaNetworkName + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for all SQL Servers returned by Get-DbaRegisteredServer + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias('cn', 'host', 'ServerInstance', 'Server', 'SqlInstance')] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('FastParrot')] + [switch]$Turbo, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($Computer in $ComputerName) { + $conn = $ipaddress = $null + + $OGComputer = $Computer + + if ($Computer.IsLocalhost) { + $Computer = $env:COMPUTERNAME + } + else { + $Computer = $Computer.ComputerName + } + + if ($Turbo) { + try { + Write-Message -Level VeryVerbose -Message "Resolving $Computer using .NET.Dns GetHostEntry" + $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + Write-Message -Level VeryVerbose -Message "Resolving $ipaddress using .NET.Dns GetHostByAddress" + $fqdn = [System.Net.Dns]::GetHostByAddress($ipaddress).HostName + } + catch { + try { + Write-Message -Level VeryVerbose -Message "Resolving $Computer and IP using .NET.Dns GetHostEntry" + $resolved = [System.Net.Dns]::GetHostEntry($Computer) + $ipaddress = $resolved.AddressList[0].IPAddressToString + $fqdn = $resolved.HostName + } + catch { + Stop-Function -Message "DNS name not found" -Continue -InnerErrorRecord $_ + } + } + + if ($fqdn -notmatch "\.") { + if ($computer.ComputerName -match "\.") { + $dnsdomain = $computer.ComputerName.Substring($computer.ComputerName.IndexOf(".") + 1) + $fqdn = "$resolved.$dnsdomain" + } + else { + $dnsdomain = "$env:USERDNSDOMAIN".ToLower() + if ($dnsdomain -match "\.") { + $fqdn = "$fqdn.$dnsdomain" + } + } + } + + $hostname = $fqdn.Split(".")[0] + + [PSCustomObject]@{ + InputName = $OGComputer + ComputerName = $hostname.ToUpper() + IPAddress = $ipaddress + DNSHostname = $hostname + DNSDomain = $fqdn.Replace("$hostname.", "") + Domain = $fqdn.Replace("$hostname.", "") + DNSHostEntry = $fqdn + FQDN = $fqdn + FullComputerName = $fqdn + } + + } + else { + + Write-Message -Level Verbose -Message "Connecting to $Computer" + + try { + $ipaddress = ((Test-Connection -ComputerName $Computer -Count 1 -ErrorAction Stop).Ipv4Address).IPAddressToString + } + catch { + try { + if ($env:USERDNSDOMAIN) { + $ipaddress = ((Test-Connection -ComputerName "$Computer.$env:USERDNSDOMAIN" -Count 1 -ErrorAction SilentlyContinue).Ipv4Address).IPAddressToString + $Computer = "$Computer.$env:USERDNSDOMAIN" + } + } + catch { + $Computer = $OGComputer + $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + } + } + + if ($ipaddress) { + Write-Message -Level VeryVerbose -Message "IP Address from $Computer is $ipaddress" + } + else { + Write-Message -Level VeryVerbose -Message "No IP Address returned from $Computer" + Write-Message -Level VeryVerbose -Message "Using .NET.Dns to resolve IP Address" + return (Resolve-DbaNetworkName -ComputerName $Computer -Turbo) + } + + if ($PSVersionTable.PSVersion.Major -gt 2) { + Write-Message -Level System -Message "Your PowerShell Version is $($PSVersionTable.PSVersion.Major)" + try { + try { + # if an alias (CNAME) is passed we should try to connect to the A name via CIM or WinRM + $ComputerNameIP = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + $RemoteComputer = [System.Net.Dns]::GetHostByAddress($ComputerNameIP).HostName + } + catch { + $RemoteComputer = $Computer + } + Write-Message -Level VeryVerbose -Message "Getting computer information from $RemoteComputer" + $ScBlock = { + $IPGProps = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() + return [pscustomobject]@{ + 'DNSDomain' = $IPGProps.DomainName + } + } + if (Test-Bound "Credential") { + $conn = Get-DbaCmObject -ClassName win32_ComputerSystem -Computer $RemoteComputer -Credential $Credential -EnableException + $DNSSuffix = Invoke-Command2 -Computer $RemoteComputer -ScriptBlock $ScBlock -Credential $Credential -ErrorAction Stop + } + else { + $conn = Get-DbaCmObject -ClassName win32_ComputerSystem -Computer $RemoteComputer -EnableException + $DNSSuffix = Invoke-Command2 -Computer $RemoteComputer -ScriptBlock $ScBlock -ErrorAction Stop + } + } + catch { + Write-Message -Level Verbose -Message "Unable to get computer information from $Computer" + } + + if (!$conn) { + Write-Message -Level Verbose -Message "No WMI/CIM from $Computer. Getting HostName via .NET.Dns" + try { + $fqdn = ([System.Net.Dns]::GetHostEntry($Computer)).HostName + $hostname = $fqdn.Split(".")[0] + $suffix = $fqdn.Replace("$hostname.", "") + if ($hostname -eq $fqdn) { + $suffix = "" + } + $conn = [PSCustomObject]@{ + Name = $Computer + DNSHostname = $hostname + Domain = $suffix + } + $DNSSuffix = [PSCustomObject]@{ + DNSDomain = $suffix + } + } + catch { + Stop-Function -Message "No .NET.Dns information from $Computer" -InnerErrorRecord $_ -Continue + } + } + } + if ($DNSSuffix.DNSDomain.Length -eq 0) { + $FullComputerName = $conn.DNSHostname + } + else { + $FullComputerName = $conn.DNSHostname + "." + $DNSSuffix.DNSDomain + } + try { + Write-Message -Level VeryVerbose -Message "Resolving $FullComputerName using .NET.Dns GetHostEntry" + $hostentry = ([System.Net.Dns]::GetHostEntry($FullComputerName)).HostName + } + catch { + Stop-Function -Message ".NET.Dns GetHostEntry failed for $FullComputerName" -InnerErrorRecord $_ + } + + $fqdn = "$($conn.DNSHostname).$($conn.Domain)" + if ($fqdn -eq ".") { + Write-Message -Level VeryVerbose -Message "No full FQDN found. Setting to null" + $fqdn = $null + } + if ($FullComputerName -eq ".") { + Write-Message -Level VeryVerbose -Message "No DNS FQDN found. Setting to null" + $FullComputerName = $null + } + + if ($FullComputerName -ne "." -and $FullComputerName -notmatch "\." -and $conn.Domain -match "\.") { + $d = $conn.Domain + $FullComputerName = "$FullComputerName.$d" + } + + [PSCustomObject]@{ + InputName = $OGComputer + ComputerName = $conn.Name + IPAddress = $ipaddress + DNSHostName = $conn.DNSHostname + DNSDomain = $DNSSuffix.DNSDomain + Domain = $conn.Domain + DNSHostEntry = $hostentry + FQDN = $fqdn.TrimEnd(".") + FullComputerName = $FullComputerName + } + } + } + } +} +function Restart-DbaSqlService { + <# + .SYNOPSIS + Restarts SQL Server services on a computer. + + .DESCRIPTION + Restarts the SQL Server related services on one or more computers. Will follow SQL Server service dependencies. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER InstanceName + Only affects services that belong to the specific instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER Type + Use -Type to collect only services of the desired SqlServiceType. + Can be one of the following: "Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS" + + .PARAMETER Timeout + How long to wait for the start/stop request completion before moving on. Specify 0 to wait indefinitely. + + .PARAMETER InputObject + A collection of services from Get-DbaSqlService + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER Force + Will stop dependent SQL Server agents when stopping Engine services. + + .NOTES + Tags: Service, SqlServer, Instance, Connect + Author: Kirill Kravtsov( @nvarscar ) + + Requires Local Admin rights on destination computer(s). + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2017 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restart-DbaSqlService + + .EXAMPLE + Restart-DbaSqlService -ComputerName sqlserver2014a + + Restarts the SQL Server related services on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3'| Get-DbaSqlService | Restart-DbaSqlService + + Gets the SQL Server related services on computers sql1, sql2 and sql3 and restarts them. + + .EXAMPLE + Restart-DbaSqlService -ComputerName sql1,sql2 -Instance MSSQLSERVER + + Restarts the SQL Server services related to the default instance MSSQLSERVER on computers sql1 and sql2. + + .EXAMPLE + Restart-DbaSqlService -ComputerName $MyServers -Type SSRS + + Restarts the SQL Server related services of type "SSRS" (Reporting Services) on computers in the variable MyServers. + + .EXAMPLE + Restart-DbaSqlService -ComputerName sql1 -Type Engine -Force + + Restarts SQL Server database engine services on sql1 forcing dependent SQL Server Agent services to restart as well. + #> + [CmdletBinding(DefaultParameterSetName = "Server", SupportsShouldProcess = $true)] + Param ( + [Parameter(ParameterSetName = "Server", Position = 1)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [Alias("Instance")] + [string[]]$InstanceName, + [ValidateSet("Agent", "Browser", "Engine", "FullText", "SSAS", "SSIS", "SSRS")] + [string[]]$Type, + [parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = "Service")] + [Alias("ServiceCollection")] + [object[]]$InputObject, + [int]$Timeout = 30, + [PSCredential]$Credential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $processArray = @() + if ($PsCmdlet.ParameterSetName -eq "Server") { + $serviceParams = @{ ComputerName = $ComputerName } + if ($InstanceName) { $serviceParams.InstanceName = $InstanceName } + if ($Type) { $serviceParams.Type = $Type } + if ($Credential) { $serviceParams.Credential = $Credential } + if ($EnableException) { $serviceParams.Silent = $EnableException } + $InputObject = Get-DbaSqlService @serviceParams + } + } + process { + #Get all the objects from the pipeline before proceeding + $processArray += $InputObject + } + end { + $processArray = [array]($processArray | Where-Object { (!$InstanceName -or $_.InstanceName -in $InstanceName) -and (!$Type -or $_.ServiceType -in $Type) }) + foreach ($service in $processArray) { + if ($Force -and $service.ServiceType -eq 'Engine' -and !($processArray | Where-Object { $_.ServiceType -eq 'Agent' -and $_.InstanceName -eq $service.InstanceName -and $_.ComputerName -eq $service.ComputerName })) { + Write-Message -Level Verbose -Message "Adding Agent service to the list for service $($service.ServiceName) on $($service.ComputerName), since -Force has been specified" + #Construct parameters to call Get-DbaSqlService + $serviceParams = @{ + ComputerName = $service.ComputerName + InstanceName = $service.InstanceName + Type = 'Agent' + } + if ($Credential) { $serviceParams.Credential = $Credential } + if ($EnableException) { $serviceParams.Silent = $EnableException } + $processArray += @(Get-DbaSqlService @serviceParams) + } + } + if ($processArray) { + $services = Update-ServiceStatus -InputObject $processArray -Action 'stop' -Timeout $Timeout -EnableException $EnableException + foreach ($service in ($services | Where-Object { $_.Status -eq 'Failed'})) { + $service + } + $services = $services | Where-Object { $_.Status -eq 'Successful'} + if ($services) { + Update-ServiceStatus -InputObject $services -Action 'restart' -Timeout $Timeout -EnableException $EnableException + } + } + else { Stop-Function -EnableException $EnableException -Message "No SQL Server services found with current parameters." } + } +} +function Restore-DbaBackupFromDirectory { + <# + .SYNOPSIS + Restores SQL Server databases from the backup directory structure created by Ola Hallengren's database maintenance scripts. Different structures coming soon. + + .DESCRIPTION + Many SQL Server database administrators use Ola Hallengren's SQL Server Maintenance Solution which can be found at http://ola.hallengren.com + + Hallengren uses a predictable backup structure which made it relatively easy to create a script that can restore an entire SQL Server database instance, down to the master database (next version), to a new server. This script is intended to be used in the event that the originating SQL Server becomes unavailable, thus rendering my other SQL restore script (http://goo.gl/QmfQ6s) ineffective. + + .PARAMETER SqlInstance + The SQL Server instance to which you will be restoring the database. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the full path to the directory that contains the database backups. The SQL Server service must have read access to this path. + + .PARAMETER ReuseSourceFolderStructure + If this switch is enabled, the folder structure used on the instance where the backup was made will be recreated. By default, the database files will be restored to the default data and log directories for the instance you're restoring onto. + + .PARAMETER NoRecovery + If this switch is enabled, the database is left in the No Recovery state to enable further backups to be added. + + .PARAMETER Force + If this switch is enabled, any existing database matching the name of a database being restored will be overwritten. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Requires: sysadmin access on destination SQL Server. + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restore-SqlBackupFromDirectory + + .EXAMPLE + Restore-SqlBackupFromDirectory -SqlInstance sqlcluster -Path \\fileserver\share\sqlbackups\SQLSERVER2014A + + All user databases contained within \\fileserver\share\sqlbackups\SQLSERVERA will be restored to sqlcluster, down the most recent full/differential/logs. + + #> + #Requires -Version 3.0 + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(Mandatory = $true)] + [string]$Path, + [switch]$NoRecovery, + [Alias("ReuseFolderStructure")] + [switch]$ReuseSourceFolderStructure, + [PSCredential]$SqlCredential, + [switch]$Force + ) + + Write-Warning "This command is no longer supported. Please use Get-ChildItem | Restore-DbaDatabase instead" +} +function Restore-DbaDatabase { + <# + .SYNOPSIS + Restores a SQL Server Database from a set of backupfiles + + .DESCRIPTION + Upon being passed a list of potential backups files this command will scan the files, select those that contain SQL Server + backup sets. It will then filter those files down to a set that can perform the requested restore, checking that we have a + full restore chain to the point in time requested by the caller. + + The function defaults to working on a remote instance. This means that all paths passed in must be relative to the remote instance. + XpDirTree will be used to perform the file scans + + + Various means can be used to pass in a list of files to be considered. The default is to non recursively scan the folder + passed in. + + .PARAMETER SqlInstance + The SQL Server instance to restore to. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER Path + Path to SQL Server backup files. + + Paths passed in as strings will be scanned using the desired method, default is a non recursive folder scan + Accepts multiple paths separated by ',' + + Or it can consist of FileInfo objects, such as the output of Get-ChildItem or Get-Item. This allows you to work with + your own filestructures as needed + + .PARAMETER DatabaseName + Name to restore the database under. + Only works with a single database restore. If multiple database are found in the provided paths then we will exit + + .PARAMETER DestinationDataDirectory + Path to restore the SQL Server backups to on the target instance. + If only this parameter is specified, then all database files (data and log) will be restored to this location + + .PARAMETER DestinationLogDirectory + Path to restore the database log files to. + This parameter can only be specified alongside DestinationDataDirectory. + + .PARAMETER DestinationFileStreamDirectory + Path to restore FileStream data to + This parameter can only be specified alongside DestinationDataDirectory + + .PARAMETER RestoreTime + Specify a DateTime object to which you want the database restored to. Default is to the latest point available in the specified backups + + .PARAMETER NoRecovery + Indicates if the databases should be recovered after last restore. Default is to recover + + .PARAMETER WithReplace + Switch indicated is the restore is allowed to replace an existing database. + + .PARAMETER XpDirTree + Switch that indicated file scanning should be performed by the SQL Server instance using xp_dirtree + This will scan recursively from the passed in path + You must have sysadmin role membership on the instance for this to work. + + .PARAMETER OutputScriptOnly + Switch indicates that ONLY T-SQL scripts should be generated, no restore takes place + + .PARAMETER VerifyOnly + Switch indicate that restore should be verified + + .PARAMETER MaintenanceSolutionBackup + Switch to indicate the backup files are in a folder structure as created by Ola Hallengreen's maintenance scripts. + This swith enables a faster check for suitable backups. Other options require all files to be read first to ensure we have an anchoring full backup. Because we can rely on specific locations for backups performed with OlaHallengren's backup solution, we can rely on file locations. + + .PARAMETER FileMapping + A hashtable that can be used to move specific files to a location. + $FileMapping = @{'DataFile1'='c:\restoredfiles\Datafile1.mdf';'DataFile3'='d:\DataFile3.mdf'} + And files not specified in the mapping will be restored to their original location + This Parameter is exclusive with DestinationDataDirectory + + .PARAMETER IgnoreLogBackup + This switch tells the function to ignore transaction log backups. The process will restore to the latest full or differential backup point only + + .PARAMETER useDestinationDefaultDirectories + Switch that tells the restore to use the default Data and Log locations on the target server. If they don't exist, the function will try to create them + + .PARAMETER ReuseSourceFolderStructure + By default, databases will be migrated to the destination Sql Server's default data and log directories. You can override this by specifying -ReuseSourceFolderStructure. + The same structure on the SOURCE will be kept exactly, so consider this if you're migrating between different versions and use part of Microsoft's default Sql structure (MSSql12.INSTANCE, etc) + + *Note, to reuse destination folder structure, specify -WithReplace + + .PARAMETER DestinationFilePrefix + This value will be prefixed to ALL restored files (log and data). This is just a simple string prefix. If you want to perform more complex rename operations then please use the FileMapping parameter + + This will apply to all file move options, except for FileMapping + + .PARAMETER DestinationFileSuffix + This value will be suffixed to ALL restored files (log and data). This is just a simple string suffix. If you want to perform more complex rename operations then please use the FileMapping parameter + + This will apply to all file move options, except for FileMapping + + .PARAMETER RestoredDatabaseNamePrefix + A string which will be prefixed to the start of the restore Database's Name + Useful if restoring a copy to the same sql server for testing. + + .PARAMETER TrustDbBackupHistory + This switch can be used when piping the output of Get-DbaBackupHistory or Backup-DbaDatabase into this command. + It allows the user to say that they trust that the output from those commands is correct, and skips the file header read portion of the process. This means a faster process, but at the risk of not knowing till halfway through the restore that something is wrong with a file. + + .PARAMETER MaxTransferSize + Parameter to set the unit of transfer. Values must be a multiple by 64kb + + .PARAMETER Blocksize + Specifies the block size to use. Must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb or 64kb + Can be specified in bytes + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER BufferCount + Number of I/O buffers to use to perform the operation. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER XpNoRecurse + If specified, prevents the XpDirTree process from recursing (its default behaviour) + + .PARAMETER DirectoryRecurse + If specified the specified directory will be recursed into + + .PARAMETER Continue + If specified we will to attempt to recover more transaction log backups onto database(s) in Recovering or Standby states + + .PARAMETER StandbyDirectory + If a directory is specified the database(s) will be restored into a standby state, with the standby file placed into this directory (which must exist, and be writable by the target Sql Server instance) + + .PARAMETER AzureCredential + The name of the SQL Server credential to be used if restoring from an Azure hosted backup + + .PARAMETER ReplaceDbNameInFile + If switch set and occurence of the original database's name in a data or log file will be replace with the name specified in the Databasename parameter + + .PARAMETER Recover + If set will perform recovery on the indicated database + + .PARAMETER AllowContinue + By default, Restore-DbaDatabase will stop restoring any databases if it comes across an error. + Use this switch to enable it to restore all databases without issues. + + .PARAMETER GetBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Get-DbaBackupInformation + + .PARAMETER SelectBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Select-DbaBackupInformation + + .PARAMETER FormatBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Format-DbaBackupInformation + + .PARAMETER TestBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Test-DbaBackupInformation + + .PARAMETER StopAfterGetBackupInformation + Switch which will cause the function to exit after returning GetBackupInformation + + .PARAMETER StopAfterSelectBackupInformation + Switch which will cause the function to exit after returning SelectBackupInformation + + .PARAMETER StopAfterFormatBackupInformation + Switch which will cause the function to exit after returning FormatBackupInformation + + .PARAMETER StopAfterTestBackupInformation + Switch which will cause the function to exit after returning TestBackupInformation + + .PARAMETER StatementTimeOut + Timeout in minutes. Defaults to infinity (restores can take a while.) + + .PARAMETER KeepCDC + Indicates whether CDC information should be restored as part of the database + + .PARAMETER PageRestore + Passes in an object from Get-DbaSuspectPages containing suspect pages from a single database. + Setting this Parameter will cause an Online Page restore if the target Instance is Enterprise Edition, or offline if not. + This will involve taking a tail log backup, so you must check your restore chain once it has completed + + .PARAMETER PageRestoreTailFolder + This parameter passes in a location for the tail log backup required for page level restore + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts to confirm certain actions + + .PARAMETER WhatIf + Shows what would happen if the command would execute, but does not actually perform the command + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups + + Scans all the backup files in \\server2\backups, filters them and restores the database to server1\instance1 + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores + + Scans all the backup files in \\server2\backups$ stored in an Ola Hallengren style folder structure, + filters them and restores the database to the c:\restores folder on server1\instance1 + + .EXAMPLE + Get-ChildItem c:\SQLbackups1\, \\server\sqlbackups2 | Restore-DbaDatabase -SqlInstance server1\instance1 + + Takes the provided files from multiple directories and restores them on server1\instance1 + + .EXAMPLE + $RestoreTime = Get-Date('11:19 23/12/2016') + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores -RestoreTime $RestoreTime + + Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, + filters them and restores the database to the c:\restores folder on server1\instance1 up to 11:19 23/12/2016 + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -DestinationDataDirectory c:\restores -OutputScriptOnly | Select-Object -ExpandProperty Tsql | Out-File -Filepath c:\scripts\restore.sql + + Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, + filters them and generate the T-SQL Scripts to restore the database to the latest point in time, + and then stores the output in a file for later retrieval + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path c:\backups -DestinationDataDirectory c:\DataFiles -DestinationLogDirectory c:\LogFile + + Scans all the files in c:\backups and then restores them onto the SQL Server Instance server1\instance1, placing data files + c:\DataFiles and all the log files into c:\LogFiles + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path http://demo.blob.core.windows.net/backups/dbbackup.bak -AzureCredential MyAzureCredential + + Will restore the backup held at http://demo.blob.core.windows.net/backups/dbbackup.bak to server1\instance1. The connection to Azure will be made using the + credential MyAzureCredential held on instance Server1\instance1 + + .EXAMPLE + $File = Get-ChildItem c:\backups, \\server1\backups -recurse + $File | Restore-DbaDatabase -SqlInstance Server1\Instance -useDestinationDefaultDirectories + + This will take all of the files found under the folders c:\backups and \\server1\backups, and pipeline them into + Restore-DbaDatabase. Restore-DbaDatabase will then scan all of the files, and restore all of the databases included + to the latest point in time covered by their backups. All data and log files will be moved to the default SQL Server + folder for those file types as defined on the target instance. + + .EXAMPLE + $files = Get-ChildItem C:\dbatools\db1 + + #Restore database to a point in time + $files | Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -RestoreTime (get-date "14:58:30 22/05/2017") ` + -NoRecovery -WithReplace -StandbyDirectory C:\dbatools\standby + + #It's in standby so we can peek at it + Invoke-Sqlcmd2 -ServerInstance server\instance1 -Query "select top 1 * from Restored.dbo.steps order by dt desc" + + #Not quite there so let's roll on a bit: + $files | Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -continue -WithReplace -RestoreTime (get-date "15:09:30 22/05/2017") ` + -StandbyDirectory C:\dbatools\standby + + Invoke-Sqlcmd2 -ServerInstance server\instance1 -Query "select top 1 * from restored.dbo.steps order by dt desc" + + Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -continue -WithReplace + + In this example we step through the backup files held in c:\dbatools\db1 folder. + First we restore the database to a point in time in standby mode. This means we can check some details in the databases + We then roll it on a further 9 minutes to perform some more checks + And finally we continue by rolling it all the way forward to the latest point in the backup. + At each step, only the log files needed to roll the database forward are restored. + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server\instance1 -Path c:\backups -DatabaseName example1 -WithNoRecovery + Restore-DbaDatabase -SqlInstance server\instance1 -Recover -DatabaseName example1 + + .EXAMPLE + $SuspectPage = Get-DbaSuspectPage -SqlInstance server\instance1 -Database ProdFinance + Get-DbaBackupHistory - SqlInstance server\instance1 -Database -ProdFinance -Last | Restore-DbaDatabase -PageRestore $SuspectPage -PageRestoreTailFolder c:\temp -TrustDbBackupHistory -AllowContinues + + Gets a list of Suspect Pages using Get-DbaSuspectPage. The uses Get-DbaBackupHistory and Restore-DbaDatabase to perform a restore of the suspect pages and bring them up to date + If server\instance1 is Enterprise edition this will be done online, if not it will be performed offline + AllowContinue is required to make sure we cope with existing files + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Author: Stuart Moore (@napalmgram), stuart-moore.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Restore")] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Restore")] + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "RestorePage")] + [object[]]$Path, + [parameter(ValueFromPipeline = $true)] + [Alias("Name")] + [object[]]$DatabaseName, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationDataDirectory, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationLogDirectory, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationFileStreamDirectory, + [parameter(ParameterSetName = "Restore")] + [DateTime]$RestoreTime = (Get-Date).AddYears(1), + [parameter(ParameterSetName = "Restore")] + [switch]$NoRecovery, + [parameter(ParameterSetName = "Restore")] + [switch]$WithReplace, + [parameter(ParameterSetName = "Restore")] + [Switch]$XpDirTree, + [switch]$OutputScriptOnly, + [parameter(ParameterSetName = "Restore")] + [switch]$VerifyOnly, + [parameter(ParameterSetName = "Restore")] + [switch]$MaintenanceSolutionBackup, + [parameter(ParameterSetName = "Restore")] + [hashtable]$FileMapping, + [parameter(ParameterSetName = "Restore")] + [switch]$IgnoreLogBackup, + [parameter(ParameterSetName = "Restore")] + [switch]$useDestinationDefaultDirectories, + [parameter(ParameterSetName = "Restore")] + [switch]$ReuseSourceFolderStructure, + [parameter(ParameterSetName = "Restore")] + [string]$DestinationFilePrefix = '', + [parameter(ParameterSetName = "Restore")] + [Alias("RestoredDatababaseNamePrefix")] + [string]$RestoredDatabaseNamePrefix, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [switch]$TrustDbBackupHistory, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$MaxTransferSize, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$BlockSize, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$BufferCount, + [parameter(ParameterSetName = "Restore")] + [switch]$DirectoryRecurse, + [switch]$EnableException , + [parameter(ParameterSetName = "Restore")] + [string]$StandbyDirectory, + [parameter(ParameterSetName = "Restore")] + [switch]$Continue, + [string]$AzureCredential, + [parameter(ParameterSetName = "Restore")] + [switch]$ReplaceDbNameInFile, + [parameter(ParameterSetName = "Restore")] + [string]$DestinationFileSuffix, + [parameter(ParameterSetName = "Recovery")] + [switch]$Recover, + [parameter(ParameterSetName = "Restore")] + [switch]$KeepCDC, + [switch]$AllowContinue, + [string]$GetBackupInformation, + [switch]$StopAfterGetBackupInformation, + [string]$SelectBackupInformation, + [switch]$StopAfterSelectBackupInformation, + [string]$FormatBackupInformation, + [switch]$StopAfterFormatBackupInformation, + [string]$TestBackupInformation, + [switch]$StopAfterTestBackupInformation, + [parameter(Mandatory = $true, ParameterSetName = "RestorePage")] + [object]$PageRestore, + [parameter(Mandatory = $true, ParameterSetName = "RestorePage")] + [string]$PageRestoreTailFolder, + [int]$StatementTimeout = 0 + + ) + begin { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Debug -Message "Parameters bound: $($PSBoundParameters.Keys -join ", ")" + #[string]$DatabaseName = 'testparam' + #region Validation + try { + $RestoreInstance = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + if ($PSCmdlet.ParameterSetName -eq "Restore") { + $useDestinationDefaultDirectories = $true + $paramCount = 0 + + if (!(Test-Bound "AllowContinue") -and $true -ne $AllowContinue) { + $AllowContinue = $false + } + if (Test-Bound "FileMapping") { + $paramCount += 1 + } + if (Test-Bound "ReuseSourceFolderStructure") { + $paramCount += 1 + } + if (Test-Bound "DestinationDataDirectory") { + $paramCount += 1 + } + if ($paramCount -gt 1) { + Stop-Function -Category InvalidArgument -Message "You've specified incompatible Location parameters. Please only specify one of FileMapping, ReuseSourceFolderStructure or DestinationDataDirectory" + return + } + if (($ReplaceDbNameInFile) -and !(Test-Bound "DatabaseName")) { + Stop-Function -Category InvalidArgument -Message "To use ReplaceDbNameInFile you must specify DatabaseName" + return + } + + if ((Test-Bound "DestinationLogDirectory") -and (Test-Bound "ReuseSourceFolderStructure")) { + Stop-Function -Category InvalidArgument -Message "The parameters DestinationLogDirectory and UseDestinationDefaultDirectories are mutually exclusive" + return + } + if ((Test-Bound "DestinationLogDirectory") -and -not (Test-Bound "DestinationDataDirectory")) { + Stop-Function -Category InvalidArgument -Message "The parameter DestinationLogDirectory can only be specified together with DestinationDataDirectory" + return + } + if ((Test-Bound "DestinationFileStreamDirectory") -and (Test-Bound "ReuseSourceFolderStructure")) { + Stop-Function -Category InvalidArgument -Message "The parameters DestinationFileStreamDirectory and UseDestinationDefaultDirectories are mutually exclusive" + return + } + if ((Test-Bound "DestinationFileStreamDirectory") -and -not (Test-Bound "DestinationDataDirectory")) { + Stop-Function -Category InvalidArgument -Message "The parameter DestinationFileStreamDirectory can only be specified together with DestinationDataDirectory" + return + } + if (($null -ne $FileMapping) -or $ReuseSourceFolderStructure -or ($DestinationDataDirectory -ne '')) { + $useDestinationDefaultDirectories = $false + } + if (($MaxTransferSize % 64kb) -ne 0 -or $MaxTransferSize -gt 4mb) { + Stop-Function -Category InvalidArgument -Message "MaxTransferSize value must be a multiple of 64kb and no greater than 4MB" + return + } + if ($BlockSize) { + if ($BlockSize -notin (0.5kb, 1kb, 2kb, 4kb, 8kb, 16kb, 32kb, 64kb)) { + Stop-Function -Category InvalidArgument -Message "Block size must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb,64kb" + return + } + } + if ('' -ne $StandbyDirectory) { + if (!(Test-DbaSqlPath -Path $StandbyDirectory -SqlInstance $RestoreInstance)) { + Stop-Function -Message "$SqlSever cannot see the specified Standby Directory $StandbyDirectory" -Target $SqlInstance + return + } + } + if ($KeepCDC -and ($NoRecovery -or ('' -ne $StandbyDirectory))) { + Stop-Function -Category InvalidArgument -Message "KeepCDC cannot be specified with Norecovery or Standby as it needs recovery to work" + return + } + if ($Continue) { + $ContinuePoints = Get-RestoreContinuableDatabase -SqlInstance $RestoreInstance + #$WithReplace = $true + #$ContinuePoints + } + if (!($PSBoundParameters.ContainsKey("DataBasename"))) { + $PipeDatabaseName = $true + } + + } + + + + # changing statement timeout to $StatementTimeout + if ($StatementTimeout -eq 0) { + Write-Message -Level Verbose -Message "Changing statement timeout to infinity" + } + else { + Write-Message -Level Verbose -Message "Changing statement timeout to ($StatementTimeout) minutes" + } + $RestoreInstance.ConnectionContext.StatementTimeout = ($StatementTimeout * 60) + #endregion Validation + + $isLocal = [dbavalidate]::IsLocalHost($SqlInstance.ComputerName) + + if ($useDestinationDefaultDirectories) { + $DefaultPath = (Get-DbaDefaultPath -SqlInstance $RestoreInstance) + $DestinationDataDirectory = $DefaultPath.Data + $DestinationLogDirectory = $DefaultPath.Log + } + + $BackupHistory = @() + #$useDestinationDefaultDirectories = $true + } + process { + if (Test-FunctionInterrupt) { return } + if ($PSCmdlet.ParameterSetName -like "Restore*") { + if ($PipeDatabaseName -eq $true) {$DatabaseName = ''} + Write-Message -message "ParameterSet = Restore" -Level Verbose + if ($TrustDbBackupHistory -or $path[0].GetType().ToString() -eq 'Sqlcollaborative.Dbatools.Database.BackupHistory') { + foreach ($f in $path) { + Write-Message -Level Verbose -Message "Trust Database Backup History Set" + if ("BackupPath" -notin $f.PSobject.Properties.name) { + Write-Message -Level Verbose -Message "adding BackupPath - $($_.Fullname)" + $f = $f | Select-Object *, @{ Name = "BackupPath"; Expression = { $_.FullName } } + } + if ("DatabaseName" -notin $f.PSobject.Properties.name) { + $f = $f | Select-Object *, @{ Name = "DatabaseName"; Expression = { $_.Database } } + } + if ("Database" -notin $f.PSobject.Properties.name) { + $f = $f | Select-Object *, @{ Name = "Database"; Expression = { $_.DatabaseName } } + } + if ("Type" -notin $f.PSobject.Properties.name) { + #$f = $f | Select-Object *, @{Name="Type";Expression={"Full"}} + } + if ("BackupSetGUID" -notin $f.PSobject.Properties.name) { + #This line until Get-DbaBackupHistory gets fixed + #$f = $f | Select-Object *, @{ Name = "BackupSetGUID"; Expression = { $_.BackupSetupID } } + #This one once it's sorted: + $f = $f | Select-Object *, @{Name = "BackupSetGUID"; Expression = {$_.BackupSetID}} + } + if ($f.BackupPath -like 'http*' -and '' -eq $AzureCredential) { + Stop-Function -Message "At least one Azure backup passed in, and no Credential supplied. Stopping" + return + } + + $BackupHistory += $F | Select-Object *, @{ Name = "ServerName"; Expression = { $_.SqlInstance } }, @{ Name = "BackupStartDate"; Expression = { $_.Start -as [DateTime] } } + + } + } + else { + $files = @() + foreach ($f in $Path) { + if ($f -is [System.IO.FileSystemInfo]) { + $files += $f.fullname + } + else { + $files += $f + } + } + Write-Message -Level Verbose -Message "Unverified input, full scans - $($files -join ';')" + $BackupHistory += Get-DbaBackupInformation -SqlInstance $RestoreInstance -SqlCredential $SqlCredential -Path $files -DirectoryRecurse:$DirectoryRecurse -MaintenanceSolution:$MaintenanceSolutionBackup -IgnoreLogBackup:$IgnoreLogBackup -AzureCredential $AzureCredential + } + if ($PSCmdlet.ParameterSetName -eq "RestorePage") { + if (-not (Test-DbaSqlPath -SqlInstance $RestoreInstance -Path $PageRestoreTailFolder)) { + Stop-Function -Message "Instance $RestoreInstance cannot read $PageRestoreTailFolder, cannot proceed" -Target $PageRestoreTailFolder + return + } + $WithReplace = $true + } + } + elseif ($PSCmdlet.ParameterSetName -eq "Recovery") { + Write-Message -Message "$($Database.count) databases to recover" -level Verbose + ForEach ($DataBase in $DatabaseName) { + if ($database -is [object]) { + #We've got an object, try the normal options Database, DatabaseName, Name + if ("Database" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.Database + } + elseif ("DatabaseName" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.DatabaseName + } + elseif ("Name" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.name + } + } + Write-Verbose "existence - $($RestoreInstance.Databases[$DataBase].State)" + if ($RestoreInstance.Databases[$DataBase].State -ne 'Existing') { + Write-Message -Message "$Database does not exist on $RestoreInstance" -level Warning + Continue + + } + if ($RestoreInstance.Databases[$Database].Status -ne "Restoring") { + Write-Message -Message "$Database on $RestoreInstance is not in a Restoring State" -Level Warning + Continue + + } + $RestoreComplete = $true + $RecoverSql = "RESTORE DATABASE $Database WITH RECOVERY" + Write-Message -Message "Recovery Sql Query - $RecoverSql" -level verbose + Try { + $RestoreInstance.query($RecoverSql) + } + Catch { + $RestoreComplete = $False + $ExitError = $_.Exception.InnerException + Write-Message -Level Warning -Message "Failed to recover $Database on $RestoreInstance, `n $ExitError" + } + Finally { + [PSCustomObject]@{ + SqlInstance = $SqlInstance + DatabaseName = $Database + RestoreComplete = $RestoreComplete + Scripts = $RecoverSql + } + } + } + } + } + end { + if (Test-FunctionInterrupt) { return } + if ($PSCmdlet.ParameterSetName -like "Restore*") { + if ($BackupHistory.Count -eq 0) { + Write-Message -Level Warning -Message "No backups passed through. `n This could mean the SQL instance cannot see the referenced files, the file's headers could not be read or some other issue" + return + } + Write-Message -message "Processing DatabaseName - $DatabaseName" -Level Verbose + $FilteredBackupHistory = @() + if (Test-Bound -ParameterName GetBackupInformation) { + Write-Message -Message "Setting $GetBackupInformation to BackupHistory" -Level Verbose + Set-Variable -Name $GetBackupInformation -Value $BackupHistory -Scope Global + } + if ($StopAfterGetBackupInformation) { + return + } + + $FilteredBackupHistory = $BackupHistory | Select-DbaBackupInformation -RestoreTime $RestoreTime -IgnoreLogs:$IgnoreLogBackups -ContinuePoints $ContinuePoints + + if ( Test-Bound -ParameterName SelectBackupInformation) { + Write-Message -Message "Setting $SelectBackupInformation to FilteredBackupHistory" -Level Verbose + Set-Variable -Name $SelectBackupInformation -Value $FilteredBackupHistory -Scope Global + + } + if ($StopAfterSelectBackupInformation) { + return + } + + $null = $FilteredBackupHistory | Format-DbaBackupInformation -DataFileDirectory $DestinationDataDirectory -LogFileDirectory $DestinationLogDirectory -DestinationFileStreamDirectory $DestinationFileStreamDirectory -DatabaseFileSuffix $DestinationFileSuffix -DatabaseFilePrefix $DestinationFilePrefix -DatabaseNamePrefix $RestoredDatabaseNamePrefix -ReplaceDatabaseName $DatabaseName -Continue:$Continue -ReplaceDbNameInFile:$ReplaceDbNameInFile -FileMapping $FileMapping + + if ( Test-Bound -ParameterName FormatBackupInformation) { + Set-Variable -Name $FormatBackupInformation -Value $FilteredBackupHistory -Scope Global + } + if ($StopAfterFormatBackupInformation) { + return + } + + try { + Write-Message -Level Verbose -Message "VerifyOnly = $VerifyOnly" + $null = $FilteredBackupHistory | Test-DbaBackupInformation -SqlInstance $RestoreInstance -WithReplace:$WithReplace -Continue:$Continue -VerifyOnly:$VerifyOnly -EnableException:$true + } + catch { + Stop-Function -ErrorRecord $_ -Message "Failure" -Continue + } + + if ( Test-Bound -ParameterName TestBackupInformation) { + Set-Variable -Name $TestBackupInformation -Value $FilteredBackupHistory -Scope Global + } + if ($StopAfterTestBackupInformation) { + return + } + $DbVerfied = ($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $True} | Select-Object -Property Database -Unique).Database -join ',' + Write-Message -Message "$DbVerfied passed testing" -Level Verbose + + if (($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $True}).count -lt $FilteredBackupHistory.count) { + $DbUnVerified = ($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $False} | Select-Object -Property Database -Unique).Database -join ',' + if ($AllowContinue) { + Write-Message -Message "$DbUnverified failed testing, AllowContinue set" -Level Verbose + } + else { + Stop-Function -Message "Database $DbUnverified failed testing, AllowContinue not set, exiting" + return + } + } + + If ($PSCmdlet.ParameterSetName -eq "RestorePage") { + if (($FilteredBackupHistory.Database | select-Object -unique | Measure-Object).count -ne 1) { + Stop-Function -Message "Must only 1 database passed in for Page Restore. Sorry" + return + } + else { + $WithReplace = $false + $PageDb = ($FilteredBackupHistory.Database | select-Object -unique).Database + } + } + Write-Message -Message "Passing in to restore" -Level Verbose + if ($PSCmdlet.ParameterSetName -eq "RestorePage" -and $RestoreInstance.Edition -notlike '*Enterprise*') { + Write-Message -Message "Taking Tail log backup for page restore for non-Enterprise" -Level Verbose + $TailBackup = Backup-DbaDatabase -SqlInstance $RestoreInstance -Database $DatabaseName -Type Log -BackupDirectory $PageRestoreTailFolder -Norecovery -CopyOnly + } + $FilteredBackupHistory | Where-Object {$_.IsVerified -eq $true} | Invoke-DbaAdvancedRestore -SqlInstance $RestoreInstance -WithReplace:$WithReplace -RestoreTime $RestoreTime -StandbyDirectory $StandbyDirectory -NoRecovery:$NoRecovery -Continue:$Continue -OutputScriptOnly:$OutputScriptOnly -BlockSize $BlockSize -MaxTransferSize $MaxTransferSize -Buffercount $Buffercount -KeepCDC:$KeepCDC -VerifyOnly:$VerifyOnly -PageRestore $PageRestore + if ($PSCmdlet.ParameterSetName -eq "RestorePage" ) { + if ($RestoreInstace.Edition -like '*Enterprise*') { + Write-Message -Message "Taking Tail log backup for page restore for Enterprise" -Level Verbose + $TailBackup = Backup-DbaDatabase -SqlInstance $RestoreInstance -Database $DatabaseName -Type Log -BackupDirectory $PageRestoreTailFolder -Norecovery -CopyOnly + } + Write-Message -Message "Restoring Tail log backup for page restore" -Level Verbose + $TailBackup | Restore-DbaDatabase -SqlInstance $RestoreInstance -TrustDbBackupHistory -NoRecovery -OutputScriptOnly:$OutputScriptOnly -BlockSize $BlockSize -MaxTransferSize $MaxTransferSize -Buffercount $Buffercount -Continue + Restore-DbaDatabase -SqlInstance $RestoreInstance -Recover -Database $DatabaseName -OutputScriptOnly:$OutputScriptOnly + } + + } + } +} +function Restore-DbaDbCertificate { + <# +.SYNOPSIS +Imports certificates from .cer files using SMO. + +.DESCRIPTION +Imports certificates from.cer files using SMO. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER Path +The Path the contains the certificate and private key files. The path can be a directory or a specific certificate. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Password +Secure string used to decrypt the private key. + +.PARAMETER Database +The database where the certificate imports into. Defaults to master. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Migration, Certificate +Author: Jess Pomfret (@jpomfret) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Restore-DbaDbCertificate -SqlInstance Server1 -Path \\Server1\Certificates -password (ConvertTo-SecureString -force -AsPlainText GoodPass1234!!) +Imports all the certificates in the specified path. + +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Path, + [object]$Database = "master", + [Security.SecureString]$Password = (Read-Host "Password" -AsSecureString), + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Retore-DbaDatabaseCertificate + + function new-smocert ($directory, $certname) { + if ($Pscmdlet.ShouldProcess("$cert on $SqlInstance", "Importing Certificate")) { + $smocert = New-Object Microsoft.SqlServer.Management.Smo.Certificate + $smocert.Name = $certname + $smocert.Parent = $server.Databases[$Database] + Write-Message -Level Verbose -Message "Creating Certificate: $certname" + try { + $fullcertname = "$directory\$certname.cer" + $privatekey = "$directory\$certname.pvk" + Write-Message -Level Verbose -Message "Full certificate path: $fullcertname" + Write-Message -Level Verbose -Message "Private key: $privatekey" + $fromfile = 1 + $smocert.Create($fullcertname, $fromfile, $privatekey, [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)), [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) + $smocert + } + catch { + Write-Message -Level Warning -Message $_ -ErrorRecord $_ -Target $instance + } + } + } + + try { + Write-Message -Level Verbose -Message "Connecting to $SqlInstance" + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failed to connect to: $SqlInstance" -Target $SqlInstance -InnerErrorRecord $_ + return + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($fullname in $path) { + + if (-not $SqlInstance.IsLocalHost -and -not $fullname.StartsWith('\')) { + Stop-Function -Message "Path ($fullname) must be a UNC share when SQL instance is not local." -Continue -Target $fullname + } + + if (-not (Test-DbaSqlPath -SqlInstance $server -Path $fullname)) { + Stop-Function -Message "$SqlInstance cannot access $fullname" -Continue -Target $fullname + } + + $directory = Split-Path $fullname + $filename = Split-Path $fullname -Leaf + $basename = [io.path]::GetFileNameWithoutExtension($filename) + $cert = new-smocert -directory $directory -certname $basename + Get-DbaDbCertificate -SqlInstance $server -Database $Database -Certificate $cert.Name + } + } +} +#ValidationTags#FlowControl# +function Restore-DbaDbSnapshot { + <# + .SYNOPSIS + Restores databases from snapshots + + .DESCRIPTION + Restores the database from the snapshot, discarding every modification made to the database + NB: Restoring to a snapshot will result in every other snapshot of the same database to be dropped + It also fixes some long-standing bugs in SQL Server when restoring from snapshots + + .PARAMETER SqlInstance + The SQL Server that you're connecting to + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + Restores from the last snapshot databases with this names only. You can pass either Databases or Snapshots + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Snapshot + Restores databases from snapshots with this names only. You can pass either Databases or Snapshots + + .PARAMETER Force + If restoring from a snapshot involves dropping any other shapshot, you need to explicitly + use -Force to let this command delete the ones not involved in the restore process. + Also, -Force will forcibly kill all running queries that prevent the restore process. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Backup, Restore, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restore-DbaDbSnapshot + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Restores HR and Accounting databases using the latest snapshot available + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -Force + + Restores HR database from latest snapshot and kills any active connections in the database on sqlserver2014a. + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snap_20161201, Accounting_snap_20161101 + + Restores databases from snapshots named HR_snap_20161201 and Accounting_snap_20161101 + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Snapshot, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + if (!$Snapshot -and !$Database -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify either -Snapshot (to restore from) or -Database/-ExcludeDatabase (to restore to)" + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + #$allDbs = $server.Databases + $alldbq = @" +SELECT sn.name AS Name, + dt.name AS DatabaseSnapshotBaseName, + sn.create_date AS CreateDate, + CASE + WHEN sn.source_database_id IS NOT NULL + THEN CASE + WHEN dt.state = 6 + THEN 0 + ELSE 1 + END + ELSE CASE + WHEN sn.state = 6 + THEN 0 + ELSE 1 + END + END AS IsAccessible, + CASE + WHEN sn.source_database_id IS NOT NULL + THEN 1 + ELSE 0 + END AS IsDatabaseSnapshot +FROM sys.databases sn + LEFT JOIN sys.databases dt ON sn.source_database_id = dt.database_id +"@ + $alldbs = $server.Query($alldbq) + # vault to hold all programmed operations from --> to + $operations = @() + + if (!$Snapshot -and !$Database -and !$ExcludeDatabase) { + # Restore all databases from the latest snapshot + Write-Message -Level Verbose -Message "Selected all databases" + $dbs = $allDbs | Where-Object IsDatabaseSnapshot -EQ $true + } + elseif ($Database) { + # Restore only these databases from their latest snapshot + <# + Note, for some reason lookup of this property has to be done using $_.DatabaseSnapshotBaseName + Removing this will cause: Where-Object : A positional parameter cannot be found that accepts argument 'System.Object[]' + #> + Write-Message -Level Verbose -Message "Selected only databases" + $dbs = $allDbs | Where-Object {$Database -contains $_.DatabaseSnapshotBaseName} + } + elseif ($ExcludeDatabase) { + Write-Message -Level Verbose -Message "Excluded only databases" + $dbs = $allDbs | Where-Object {$ExcludeDatabase -NotContains $_.DatabaseSnapshotBaseName} + } + elseif ($Snapshot) { + # Restore databases from these snapshots + Write-Message -Level Verbose -Message "Selected only snapshots" + $dbs = $allDbs | Where-Object { $Snapshot -contains $_.Name } + $baseDatabases = $dbs | Select-Object -ExpandProperty DatabaseSnapshotBaseName | Get-Unique + if ($baseDatabases.Count -ne $Snapshot.Count -and $dbs.Count -ne 0) { + Write-Message -Level Warning -Message "Multiple snapshots selected for the same database, skipping" + continue + } + } + $opsHash = @{ } + foreach ($db in $dbs) { + if (-not($db.IsAccessible)) { + Write-Message -Level Warning -Message "Database $db is not accessible." + continue + } + if ($db.DatabaseSnapshotBaseName -notin $opsHash.Keys) { + if ($snapshot.Count -gt 0) { + # just in the need to drop every other snapshot + $toDrop = $allDbs | Where-Object { $_.DatabaseSnapshotBaseName -eq $db.DatabaseSnapshotBaseName } + $toDrop = $todrop | Select-Object Name + $opsHash[$db.DatabaseSnapshotBaseName] = @{ + 'from' = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate + 'drop' = $toDrop + } + } + else { + $opsHash[$db.DatabaseSnapshotBaseName] = @{ + 'from' = $db + 'drop' = @() + } + } + } + else { + # store each older snapshot in the drop list while enumerating + if ($db.CreateDate -gt $opsHash[$db.DatabaseSnapshotBaseName]['from'].CreateDate) { + $prev = $opsHash[$db.DatabaseSnapshotBaseName]['from'] + $opsHash[$db.DatabaseSnapshotBaseName]['from'] = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate + $opsHash[$db.DatabaseSnapshotBaseName]['drop'] += $prev + } + } + } + foreach ($dbName in $opsHash.Keys) { + $drop = @() + foreach ($toDrop in $opsHash[$dbName]['drop']) { + $drop += $toDrop.Name + } + $operations += @{ + 'from' = $opsHash[$dbName]['from'].Name + 'to' = $dbName + 'drop' = $drop + } + } + foreach ($op in $operations) { + # Check if there are FS, because then a restore is not possible + $all_FS = $server.Databases[$op['to']].FileGroups | Where-Object FileGroupType -EQ 'FileStreamDataFileGroup' + if ($all_FS.Count -gt 0) { + Write-Message -Level Warning -Message "Database $($op['to']) has FileStream group(s). You cannot restore from snapshots" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = "Database $($op['to']) has FileStream group(s). You cannot restore from snapshots" + } + break + } + # Get log size and autogrowth + $orig_logproperties = $server.Databases[$op['to']].LogFiles | Select-Object Id, Size, Growth, GrowthType + # Drop what needs to be dropped + $opError = $false + + if ($op['drop'].Count -gt 1 -and $Force -eq $false) { + $warnMsg = @() + $warnMsg += "The restore process for $($op['to']) from $($op['from']) needs to drop the following:" + foreach ($db in $op['drop']) { + $warnMsg += $db + } + $warnMsg += "Use -Force if you really want to drop these snapshots." + Write-Message -Level Warning -Message ($warnMsg -join "`n") + break + } + foreach ($drop in $op['drop']) { + if ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $drop")) { + # skip it if it's the same name + if ($drop -ne $($op['from'])) { + try { + if ($Force) { + # snapshot with open transactions cannot be dropped + $server.KillAllProcesses($drop) + } + $null = $server.Query("USE master; DROP DATABASE [$drop]") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $operror = $true + break + } + } + } + } + if ($opError) { + Write-Message -Level Warning -Message "Errors trying to restore $($op['to']) from $($op['from'])" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = "Failed to drop some snapshots" + } + break + } + + # Need a proper restore now + if ($Pscmdlet.ShouldProcess($server.DomainInstanceName, "Restore db $($op['to']) from $($op['from'])")) { + $query = "USE master; RESTORE DATABASE [$($op['to'])] FROM DATABASE_SNAPSHOT='$($op['from'])'" + try { + if ($Force) { + # for whatever reason, a snapshot with open transactions, albeit read-only, block the restore process + $server.KillAllProcesses($op['from']) + # for a "good" reason, all open transactions on the destination block the restore process + $server.KillAllProcesses($op['to']) + } + $null = $server.Query($query) + } + catch { + $opError = $true + $inner = $_.Exception.Message + Stop-Function -Message "Original exception: $inner, Query issued: $query, Error: $_.Exception.InnerException.InnerException.Message" -ErrorRecord $_ + } + } + if ($operror) { + Write-Message -Level Warning -Message "Errors trying to restore $($op['to']) from $($op['from'])" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = '' + } + break + } + # Comparing sizes before and after, need to refresh to see if size + foreach ($log in $server.Databases[$op['to']].LogFiles) { + $log.Refresh() + } + foreach ($log in $server.Databases[$op['to']].LogFiles) { + $matching = $orig_logproperties | Where-Object ID -EQ $log.ID + $changeflag = 0 + foreach ($prop in @('Size', 'Growth', 'Growth', 'GrowthType')) { + if ($matching.$prop -ne $log.$prop) { + $changeflag = 1 + $log.$prop = $matching.$prop + } + } + if ($changeflag -ne 0) { + Write-Message -Level Verbose -Message "Restoring original settings for log file" + $log.Alter() + } + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Restored' + Notes = 'Remember to take a backup now, and also to remove the snapshot if not needed' + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Restore-DbaFromDatabaseSnapshot + } +} +function Save-DbaDiagnosticQueryScript { + <# +.SYNOPSIS +Save-DbaDiagnosticQueryScript downloads the most recent version of all Glenn Berry DMV scripts + +.DESCRIPTION +The dbatools module will have the diagnostic queries pre-installed. Use this only to update to a more recent version or specific versions. + +This function is mainly used by Invoke-DbaDiagnosticQuery, but can also be used independently to download the Glenn Berry DMV scripts. + +Use this function to pre-download the scripts from a device with an Internet connection. + +The function Invoke-DbaDiagnosticQuery will try to download these scripts automatically, but it obviously needs an internet connection to do that. + +.PARAMETER Path +Specifies the path to the output + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: André Kamman (@AndreKamman), http://clouddba.io +Tags: Diagnostic, DMV, Troubleshooting + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Save-DbaDiagnosticQueryScript -Path c:\temp + +Downloads the most recent version of all Glenn Berry DMV scripts to the specified location. +If Path is not specified, the "My Documents" location will be used. + +#> + [CmdletBinding()] + param ( + [System.IO.FileInfo]$Path = [Environment]::GetFolderPath("mydocuments"), + [Alias('Silent')] + [switch]$EnableException + ) + function Get-WebData { + param ($uri) + try { + $data = (Invoke-WebRequest -uri $uri -ErrorAction Stop) + return $data + } + catch { + Stop-Function -Message "Invoke-WebRequest failed: $_" -Target $data -ErrorRecord $_ + return + } + } + + if (-not (Test-Path $Path)) { + Stop-Function -Message "Path does not exist or access denied" -Target $path + return + } + + Add-Type -AssemblyName System.Web + $glenberryrss = "http://www.sqlskills.com/blogs/glenn/feed/" + $glenberrysql = @() + + Write-Message -Level Output -Message "Downloading RSS Feed" + $rss = [xml](get-webdata -uri $glenberryrss) + $Feed = $rss.rss.Channel + + $glenberrysql = @() + $RssPostFilter = "SQL Server Diagnostic Information Queries for*" + $DropboxLinkFilter = "*dropbox.com*" + $LinkTitleFilter = "*Diagnostic*" + + foreach ($post in $Feed.item) { + if ($post.title -like $RssPostFilter) { + # We found the first post that matches it, lets go visit and scrape. + $page = Get-WebData -uri $post.link + $glenberrysql += ($page.Links | Where-Object { $_.href -like $DropboxLinkFilter -and $_.innerText -like $LinkTitleFilter } | ForEach-Object { + [pscustomobject]@{ + URL = $_.href + SQLVersion = $_.innerText -replace " Diagnostic Information Queries", "" -replace "SQL Server ", "" -replace ' ', '' + FileYear = ($post.title -split " ")[-1] + FileMonth = "{0:00}" -f [int]([CultureInfo]::InvariantCulture.DateTimeFormat.MonthNames.IndexOf(($post.title -split " ")[-2])) + } + }) + break + } + } + Write-Message -Level Output -Message "Found $($glenberrysql.Count) documents to download" + foreach ($doc in $glenberrysql) { + try { + Write-Message -Level Output -Message "Downloading $($doc.URL)" + $page = Invoke-WebRequest -Uri $doc.URL -ErrorAction Stop + $filename = "{0}\SQLServerDiagnosticQueries_{1}_{2}.sql" -f $Path, $doc.SQLVersion, "$($doc.FileYear)$($doc.FileMonth)" + [io.file]::WriteAllText($filename, $page.content) + } + catch { + Stop-Function -Message "Requesting and writing file failed: $_" -Target $filename -ErrorRecord $_ + return + } + } +} +function Select-DbaBackupInformation { + <# + .SYNOPSIS + Select a subset of backups from a dbatools backup history object + + .DESCRIPTION + Select-DbaBackupInformation filters out a subset of backups from the dbatools backup history object with parameters supplied. + + .PARAMETER BackupHistory + A dbatools.BackupHistory object containing backup history records + + .PARAMETER RestoreTime + The point in time you want to restore to + + .PARAMETER IgnoreLogs + This switch will cause Log Backups to be ignored. So will restore to the last Full or Diff backup only + + .PARAMETER IgnoreDiffs + This switch will cause Differential backups to be ignored. Unless IgnoreLogs is specified, restore to point in time will still occur, just using all available log backups + + .PARAMETER DatabaseName + A string array of Database Names that you want to filter to + + .PARAMETER ServerName + A string array of Server Names that you want to filter + + .PARAMETER ContinuePoints + The Output of Get-RestoreContinuableDatabase while provides 'Database',redo_start_lsn,'FirstRecoveryForkID' values. Used to filter backups to continue a restore on a database + Sets IgnoreDiffs, and also filters databases to only those within the ContinuePoints object, or the ContinuePoints object AND DatabaseName if both specified + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup, Restore + Author:Stuart Moore (@napalmgram stuart-moore.com ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Select-DbaBackupInformation + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) + + Returns all backups needed to restore all the backups in \\server1\backups$ to 1 hour ago + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -DatabaseName ProdFinance + + Returns all the backups needed to restore Database ProdFinance to an hour ago + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -IgnoreLogs + + Returns all the backups in \\server1\backups$ to restore to as close prior to 1 hour ago as can be managed with only full and differential backups + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -IgnoreDiffs + + Returns all the backups in \\server1\backups$ to restore to 1 hour ago using only Full and Diff backups. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object]$BackupHistory, + [DateTime]$RestoreTime = (get-date).addmonths(1), + [switch]$IgnoreLogs, + [switch]$IgnoreDiffs, + [string[]]$DatabaseName, + [string[]]$ServerName, + [object]$ContinuePoints, + [switch]$EnableException + ) + begin { + $InternalHistory = @() + + if ((Test-Bound -ParameterName ContinuePoints) -and $null -ne $ContinuePoints) { + Write-Message -Message "ContinuePoints provided so setting up for a continue" -Level Verbose + + $IgnoreDiffs = $true + $IgnoreFull = $true + if (Test-Bound -ParameterName DatabaseName) { + $DatabaseName = $DatabaseName | Where-Object {$_ -in ($ContinuePoints | Select-Object -Property Database).Database} + + $DroppedDatabases = $DatabaseName | Where-Object {$_ -notin ($ContinuePoints | Select-Object -Property Database).Database} + if ($null -ne $DroppedDatabases) { + Write-Message -Message "$($DroppedDatabases.join(',')) filtered out as not in ContinuePoints" -Level Verbose + } + } + else { + $DatabaseName = ($ContinuePoints | Select-Object -Property Database).Database + } + } + } + process { + $internalHistory += $BackupHistory + } + + end { + ForEach ($History in $InternalHistory) { + if ("RestoreTime" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'RestoreTime' -Type NoteProperty -Value $RestoreTime + } + } + if ((Test-Bound -ParameterName DatabaseName) -and '' -ne $DatabaseName) { + Write-Message -Message "Filtering by DatabaseName" -Level Verbose + $InternalHistory = $InternalHistory | Where-Object {$_.Database -in $DatabaseName} + } + if (Test-Bound -ParameterName ServerName) { + Write-Message -Message "Filtering by ServerName" -Level Verbose + $InternalHistory = $InternalHistory | Where-Object {$_.InstanceName -in $servername} + } + + $Databases = ($InternalHistory | Select-Object -Property Database -unique).Database + ForEach ($Database in $Databases) { + Write-Message -Message "Processing Db $Database" -Level Verbose + $DatabaseHistory = $InternalHistory | Where-Object {$_.Database -eq $Database} + + $dbHistory = @() + #Find the Last Full Backup before RestoreTime + if ($true -ne $IgnoreFull) { + $Full = $DatabaseHistory | Where-Object {$_.Type -in ('Full', 'Database') -and $_.Start -le $RestoreTime} | Sort-Object -Property LastLsn -Descending | Select-Object -First 1 + $full.Fullname = ($DatabaseHistory | Where-Object {$_.Type -in ('Full', 'Database') -and $_.BackupSetID -eq $Full.BackupSetID}).Fullname + $dbHistory += $full + } + #Find the Last diff between Full and RestoreTime + if ($true -ne $IgnoreDiffs) { + $Diff = $DatabaseHistory | Where-Object {$_.Type -in ('Differential', 'Database Differential') -and $_.Start -le $RestoreTime -and $_.DatabaseBackupLSN -eq $Full.CheckpointLSN} | Sort-Object -Property LastLsn -Descending | Select-Object -First 1 + if ($null -ne $Diff) { + $Diff.FullName = ($DatabaseHistory | Where-Object {$_.Type -in ('Differential', 'Database Differential') -and $_.BackupSetID -eq $diff.BackupSetID}).Fullname + $dbhistory += $Diff + } + } + #Get All t-logs up to restore time + if ($IgnoreFull -eq $true) { + [bigint]$LogBaseLsn = ($ContinuePoints | Where-Object {$_.Database -eq $Database}).redo_start_lsn + $FirstRecoveryForkID = ($ContinuePoints | Where-Object {$_.Database -eq $Database}).FirstRecoveryForkID + Write-Message -Message "Continuing, setting fake LastLsn - $LogBaseLSN" -Level Verbose + } + else { + Write-Message -Message "Setting LogBaseLSN" -Level Verbose + [bigint]$LogBaseLsn = ($dbHistory | Sort-Object -Property LastLsn -Descending | select-object -First 1).lastLsn.ToString() + $FirstRecoveryForkID = $Full.FirstRecoveryForkID + } + + if ($true -ne $IgnoreLogs) { + $FilteredLogs = $DatabaseHistory | Where-Object {$_.Type -in ('Log', 'Transaction Log') -and $_.Start -le $RestoreTime -and $_.LastLSN.ToString() -ge $LogBaseLsn -and $_.FirstLSN -ne $_.LastLSN} | Sort-Object -Property LastLsn, FirstLsn + $GroupedLogs = $FilteredLogs | Group-Object -Property LastLSN, FirstLSN + ForEach ($Group in $GroupedLogs) { + $Log = $DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID} | select-object -First 1 + $Log.FullName = ($DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID}).Fullname + $dbhistory += $Log + #$dbhistory += $DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID} + } + # Get Last T-log + $dbHistory += $DatabaseHistory | Where-Object {$_.Type -in ('Log', 'Transaction Log') -and $_.End -ge $RestoreTime -and $_.DatabaseBackupLSN -eq $Full.CheckpointLSN} | Sort-Object -Property LastLsn, FirstLsn | Select-Object -First 1 + } + + $dbHistory #| Group-Object -Property BackupSetId # -Unique + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentAlert { + <# +.SYNOPSIS +Set-DbaAgentAlert updates a the status of a SQL Agent Alert. + +.DESCRIPTION +Set-DbaAgentAlert updates an alert in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Alert +The name of the alert. + +.PARAMETER NewName +The new name for the alert. + +.PARAMETER Enabled +Enabled the alert. + +.PARAMETER Disabled +Disabled the alert. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER InputObject +Enables piping alert objects + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Garry Bargsley (@gbargsley, garrybargsley.com) +Tags: Agent, Alert + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentAlert + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error' -Disabled +Changes the alert to disabled. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error', 'Error Number 825', 'Error Number 824' -Enabled +Changes multiple alerts to enabled. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1, sql2, sql3 -Alert 'Severity 025: Fatal Error', 'Error Number 825', 'Error Number 824' -Enabled +Changes multiple alerts to enabled on multiple servers. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error' -Disabled -Whatif +Doesn't Change the alert but shows what would happen. + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Alert, + [string]$NewName, + [switch]$Enabled, + [switch]$Disabled, + [switch]$Force, + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Agent.Alert[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { +} + process { + + if (Test-FunctionInterrupt) { return } + + if ((-not $InputObject) -and (-not $Alert)) { + Stop-Function -Message "You must specify an alert name or pipe in results from another command" -Target $sqlinstance + return + } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + foreach ($a in $Alert) { + # Check if the alert exists + if ($server.JobServer.Alerts.Name -notcontains $a) { + Stop-Function -Message "Alert $a doesn't exists on $instance" -Target $instance + } + else { + # Get the alert + try { + $InputObject += $server.JobServer.Alerts[$a] + + # Refresh the object + $InputObject.Refresh() + } + catch { + Stop-Function -Message "Something went wrong retrieving the alert" -Target $a -ErrorRecord $_ -Continue + } + } + } + } + + foreach ($currentalert in $InputObject) { + $server = $currentalert.Parent.Parent + + #region alert options + # Settings the options for the alert + if ($NewName) { + Write-Message -Message "Setting alert name to $NewName" -Level Verbose + $currentalert.Rename($NewName) + } + + if ($Enabled) { + Write-Message -Message "Setting alert to enabled" -Level Verbose + $currentalert.IsEnabled = $true + } + + if ($Disabled) { + Write-Message -Message "Setting alert to disabled" -Level Verbose + $currentalert.IsEnabled = $false + } + + #endregion alert options + + # Execute + if ($PSCmdlet.ShouldProcess($SqlInstance, "Changing the alert $a")) { + try { + Write-Message -Message "Changing the alert" -Level Verbose + + # Change the alert + $currentalert.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the alert" -ErrorRecord $_ -Target $instance -Continue + } + Get-DbaAgentAlert -SqlInstance $server | Where-Object Name -eq $currentalert.name + } + } + } +} + { + Write-Message -Message "Finished changing alert(s)" -Level Verbose + } +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentJob { + <# +.SYNOPSIS +Set-DbaAgentJob updates a job. + +.DESCRIPTION +Set-DbaAgentJob updates a job in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. + +.PARAMETER Schedule +Schedule to attach to job. This can be more than one schedule. + +.PARAMETER ScheduleId +Schedule ID to attach to job. This can be more than one schedule ID. + +.PARAMETER NewName +The new name for the job. + +.PARAMETER Enabled +Enabled the job. + +.PARAMETER Disabled +Disabled the job + +.PARAMETER Description +The description of the job. + +.PARAMETER StartStepId +The identification number of the first step to execute for the job. + +.PARAMETER Category +The category of the job. + +.PARAMETER OwnerLogin +The name of the login that owns the job. + +.PARAMETER EventlogLevel +Specifies when to place an entry in the Microsoft Windows application log for this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailLevel +Specifies when to send an e-mail upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER NetsendLevel +Specifies when to send a network message upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER PageLevel +Specifies when to send a page upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailOperator +The e-mail name of the operator to whom the e-mail is sent when EmailLevel is reached. + +.PARAMETER NetsendOperator +The name of the operator to whom the network message is sent. + +.PARAMETER PageOperator +The name of the operator to whom a page is sent. + +.PARAMETER DeleteLevel +Specifies when to delete the job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER InputObject +Enables piping job objects + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJob + +.EXAMPLE +Set-DbaAgentJob sql1 -Job Job1 -Disabled +Changes the job to disabled + +.EXAMPLE +Set-DbaAgentJob sql1 -Job Job1 -OwnerLogin user1 +Changes the owner of the job + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -EventLogLevel OnSuccess +Changes the job and sets the notification to write to the Windows Application event log on success + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -EmailLevel OnFailure -EmailOperator dba +Changes the job and sets the notification to send an e-mail to the e-mail operator + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1, Job2, Job3 -Enabled +Changes multiple jobs to enabled + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job Job1, Job2, Job3 -Enabled +Changes multiple jobs to enabled on multiple servers + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -Description 'Just another job' -Whatif +Doesn't Change the job but shows what would happen. + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job 'Job One' -Description 'Job One' +Changes a job with the name "Job1" on multiple servers to have another description + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentJob -Job Job1 -Description 'Job One' +Changes a job with the name "Job1" on multiple servers to have another description using pipe line + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Job, + [object[]]$Schedule, + [int[]]$ScheduleId, + [string]$NewName, + [switch]$Enabled, + [switch]$Disabled, + [string]$Description, + [int]$StartStepId, + [string]$Category, + [string]$OwnerLogin, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EventLogLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EmailLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$NetsendLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$PageLevel, + [string]$EmailOperator, + [string]$NetsendOperator, + [string]$PageOperator, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$DeleteLevel, + [switch]$Force, + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Agent.Job[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + # Check of the event log level is of type string and set the integer value + if (($EventLogLevel -notin 0, 1, 2, 3) -and ($null -ne $EventLogLevel)) { + $EventLogLevel = switch ($EventLogLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the email level is of type string and set the integer value + if (($EmailLevel -notin 0, 1, 2, 3) -and ($null -ne $EmailLevel)) { + $EmailLevel = switch ($EmailLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the net send level is of type string and set the integer value + if (($NetsendLevel -notin 0, 1, 2, 3) -and ($null -ne $NetsendLevel)) { + $NetsendLevel = switch ($NetsendLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the page level is of type string and set the integer value + if (($PageLevel -notin 0, 1, 2, 3) -and ($null -ne $PageLevel)) { + $PageLevel = switch ($PageLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the delete level is of type string and set the integer value + if (($DeleteLevel -notin 0, 1, 2, 3) -and ($null -ne $DeleteLevel)) { + $DeleteLevel = switch ($DeleteLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check the e-mail operator name + if (($EmailLevel -ge 1) -and (-not $EmailOperator)) { + Stop-Function -Message "Please set the e-mail operator when the e-mail level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($NetsendLevel -ge 1) -and (-not $NetsendOperator)) { + Stop-Function -Message "Please set the netsend operator when the netsend level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($PageLevel -ge 1) -and (-not $PageOperator)) { + Stop-Function -Message "Please set the page operator when the page level parameter is set." -Target $sqlinstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + if ((-not $InputObject) -and (-not $Job)) { + Stop-Function -Message "You must specify a job name or pipe in results from another command" -Target $sqlinstance + return + } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($server.JobServer.Jobs.Name -notcontains $j) { + Stop-Function -Message "Job $j doesn't exists on $instance" -Target $instance + } + else { + # Get the job + try { + $InputObject += $server.JobServer.Jobs[$j] + + # Refresh the object + $InputObject.Refresh() + } + catch { + Stop-Function -Message "Something went wrong retrieving the job" -Target $j -ErrorRecord $_ -Continue + } + } + } + } + + foreach ($currentjob in $InputObject) { + $server = $currentjob.Parent.Parent + + #region job options + # Settings the options for the job + if ($NewName) { + Write-Message -Message "Setting job name to $NewName" -Level Verbose + $currentjob.Rename($NewName) + } + + if ($Schedule) { + # Loop through each of the schedules + foreach ($s in $Schedule) { + if ($server.JobServer.SharedSchedules.Name -contains $s) { + # Get the schedule ID + $sID = $server.JobServer.SharedSchedules[$s].ID + + # Add schedule to job + Write-Message -Message "Adding schedule id $sID to job" -Level Verbose + $currentjob.AddSharedSchedule($sID) + } + else { + Stop-Function -Message "Schedule $s cannot be found on instance $instance" -Target $s -Continue + } + + } + } + + if ($ScheduleId) { + # Loop through each of the schedules IDs + foreach ($sID in $ScheduleId) { + # Check if the schedule is + if ($server.JobServer.SharedSchedules.ID -contains $sID) { + # Add schedule to job + Write-Message -Message "Adding schedule id $sID to job" -Level Verbose + $currentjob.AddSharedSchedule($sID) + + } + else { + Stop-Function -Message "Schedule ID $sID cannot be found on instance $instance" -Target $sID -Continue + } + } + } + + if ($Enabled) { + Write-Message -Message "Setting job to enabled" -Level Verbose + $currentjob.IsEnabled = $true + } + + if ($Disabled) { + Write-Message -Message "Setting job to disabled" -Level Verbose + $currentjob.IsEnabled = $false + } + + if ($Description) { + Write-Message -Message "Setting job description to $Description" -Level Verbose + $currentjob.Description = $Description + } + + if ($Category) { + # Check if the job category exists + if ($Category -notin $server.JobServer.JobCategories.Name) { + if ($Force) { + if ($PSCmdlet.ShouldProcess($instance, "Creating job category on $instance")) { + try { + # Create the category + New-DbaAgentJobCategory -SqlInstance $instance -Category $Category + + Write-Message -Message "Setting job category to $Category" -Level Verbose + $currentjob.Category = $Category + } + catch { + Stop-Function -Message "Couldn't create job category $Category from $instance" -Target $instance -ErrorRecord $_ + } + } + } + else { + Stop-Function -Message "Job category $Category doesn't exist on $instance. Use -Force to create it." -Target $instance + return + } + } + else { + Write-Message -Message "Setting job category to $Category" -Level Verbose + $currentjob.Category = $Category + } + } + + if ($StartStepId) { + # Get the job steps + $currentjobSteps = $currentjob.JobSteps + + # Check if there are any job steps + if ($currentjobSteps.Count -ge 1) { + # Check if the start step id value is one of the job steps in the job + if ($currentjobSteps.ID -contains $StartStepId) { + Write-Message -Message "Setting job start step id to $StartStepId" -Level Verbose + $currentjob.StartStepID = $StartStepId + } + else { + Write-Message -Message "The step id is not present in job $j on instance $instance" -Warning + } + + } + else { + Stop-Function -Message "There are no job steps present for job $j on instance $instance" -Target $instance -Continue + } + + } + + if ($OwnerLogin) { + # Check if the login name is present on the instance + if ($server.Logins.Name -contains $OwnerLogin) { + Write-Message -Message "Setting job owner login name to $OwnerLogin" -Level Verbose + $currentjob.OwnerLoginName = $OwnerLogin + } + else { + Stop-Function -Message "The given owner log in name $OwnerLogin does not exist on instance $instance" -Target $instance -Continue + } + } + + if ($EventLogLevel) { + Write-Message -Message "Setting job event log level to $EventlogLevel" -Level Verbose + $currentjob.EventLogLevel = $EventLogLevel + } + + if ($EmailLevel) { + # Check if the notifiction needs to be removed + if ($EmailLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToEmail = $null + + # Remove the notification + $currentjob.EmailLevel = $EmailLevel + } + else { + # Check if either the operator e-mail parameter is set or the operator is set in the job + if ($EmailOperator -or $currentjob.OperatorToEmail) { + Write-Message -Message "Setting job e-mail level to $EmailLevel" -Level Verbose + $currentjob.EmailLevel = $EmailLevel + } + else { + Stop-Function -Message "Cannot set e-mail level $EmailLevel without a valid e-mail operator name" -Target $instance -Continue + } + } + } + + if ($NetsendLevel) { + # Check if the notifiction needs to be removed + if ($NetsendLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToNetSend = $null + + # Remove the notification + $currentjob.NetSendLevel = $NetsendLevel + } + else { + # Check if either the operator netsend parameter is set or the operator is set in the job + if ($NetsendOperator -or $currentjob.OperatorToNetSend) { + Write-Message -Message "Setting job netsend level to $NetsendLevel" -Level Verbose + $currentjob.NetSendLevel = $NetsendLevel + } + else { + Stop-Function -Message "Cannot set netsend level $NetsendLevel without a valid netsend operator name" -Target $instance -Continue + } + } + } + + if ($PageLevel) { + # Check if the notifiction needs to be removed + if ($PageLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToPage = $null + + # Remove the notification + $currentjob.PageLevel = $PageLevel + } + else { + # Check if either the operator pager parameter is set or the operator is set in the job + if ($PageOperator -or $currentjob.OperatorToPage) { + Write-Message -Message "Setting job pager level to $PageLevel" -Level Verbose + $currentjob.PageLevel = $PageLevel + } + else { + Stop-Function -Message "Cannot set page level $PageLevel without a valid netsend operator name" -Target $instance -Continue + } + } + } + + # Check the current setting of the job's email level + if ($EmailOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $EmailOperator) { + Write-Message -Message "Setting job e-mail operator to $EmailOperator" -Level Verbose + $currentjob.OperatorToEmail = $EmailOperator + } + else { + Stop-Function -Message "The e-mail operator name $EmailOperator does not exist on instance $instance. Exiting.." -Target $j -Continue + } + } + + if ($NetsendOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $NetsendOperator) { + Write-Message -Message "Setting job netsend operator to $NetsendOperator" -Level Verbose + $currentjob.OperatorToNetSend = $NetsendOperator + } + else { + Stop-Function -Message "The netsend operator name $NetsendOperator does not exist on instance $instance. Exiting.." -Target $j -Continue + } + } + + if ($PageOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $PageOperator) { + Write-Message -Message "Setting job pager operator to $PageOperator" -Level Verbose + $currentjob.OperatorToPage = $PageOperator + } + else { + Stop-Function -Message "The page operator name $PageOperator does not exist on instance $instance. Exiting.." -Target $instance -Continue + } + } + + if ($DeleteLevel) { + Write-Message -Message "Setting job delete level to $DeleteLevel" -Level Verbose + $currentjob.DeleteLevel = $DeleteLevel + } + #endregion job options + + # Execute + if ($PSCmdlet.ShouldProcess($SqlInstance, "Changing the job $j")) { + try { + Write-Message -Message "Changing the job" -Level Verbose + + # Change the job + $currentjob.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the job" -ErrorRecord $_ -Target $instance -Continue + } + Get-DbaAgentJob -SqlInstance $server | Where-Object Name -eq $currentjob.name + } + } + } + + end { + Write-Message -Message "Finished changing job(s)" -Level Verbose + } +} +function Set-DbaAgentJobCategory { + <# +.SYNOPSIS +Set-DbaAgentJobCategory changes a job category. + +.DESCRIPTION +Set-DbaAgentJobCategory makes it possible to change a job category. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER NewName +New name of the job category + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJobCategory + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' -NewName 'Category 2' + +Change the name of the category from 'Category 1' to 'Category 2'. + +.EXAMPLE +Set-DbaAgentJobCategory -SqlInstance sql1, sql2 -Category Category1, Category2 -NewName cat1, cat2 + +Rename multiple jobs in one go on multiple servers. + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [string[]]$NewName, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Create array list to hold the results + $collection = New-Object System.Collections.ArrayList + + # Check if multiple categories are being changed + if ($Category.Count -gt 1 -and $NewName.Count -eq 1) { + Stop-Function -Message "You cannot rename multiple jobs to the same name" -Target $instance + } + } + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Loop through each of the categories + foreach ($cat in $Category) { + # Check if the category exists + if ($cat -notin $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat doesn't exist on $instance" -Target $instance -Continue + } + + # Check if the category already exists + if ($NewName -and ($NewName -in $server.JobServer.JobCategories.Name)) { + Stop-Function -Message "Job category $NewName already exists on $instance" -Target $instance -Continue + } + + if ($PSCmdlet.ShouldProcess($instance, "Changing the job category $Category")) { + try { + # Get the job category object + $currentCategory = $server.JobServer.JobCategories[$cat] + + Write-Message -Message "Changing job category $cat" -Level Verbose + + # Get and set the original and new values + $originalCategoryName = $currentCategory.Name + $newCategoryName = $null + + # Check if the job category needs to be renamed + if ($NewName) { + $currentCategory.Rename($NewName[$Category.IndexOf($cat)]) + $newCategoryName = $currentCategory.Name + } + + # Set up the custom object + $null = $collection.Add([PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + CategoryName = $originalCategoryName + NewCategoryName = $newCategoryName + }) + + } + catch { + Stop-Function -Message "Something went wrong changing the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } # if should process + + } # for each category + + } # foreach instance + + # Return result + return $collection + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing job category." -Level Verbose + } + +} +function Set-DbaAgentJobOutputFile { + <# + .Synopsis + Set the output file for a step within an Agent job. + + .DESCRIPTION + Sets the Output File for a step of an agent job with the Job Names and steps provided dynamically if required + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SQLCredential + Credential object used to connect to the SQL Server as a different user be it Windows or SQL Server. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Job + The job to process - this list is auto-populated from the server. + + .PARAMETER Step + The Agent Job Step to provide Output File Path for. Also available dynamically + + .PARAMETER OutputFile + The Full Path to the New Output file + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, SqlAgent + Author: Rob Sewell (https://sqldbawithabeard.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + # todo - allow piping and add -All + + .EXAMPLE + Set-DbaAgentJobOutputFile -SqlInstance SERVERNAME -JobName 'The Agent Job' -OutPutFile E:\Logs\AgentJobStepOutput.txt + + Sets the Job step for The Agent job on SERVERNAME to E:\Logs\AgentJobStepOutput.txt + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true, HelpMessage = 'The SQL Server Instance', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + Position = 0)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false, HelpMessage = 'SQL Credential', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [PSCredential]$SqlCredential, + [object[]]$Job, + [Parameter(Mandatory = $false, HelpMessage = 'The Job Step name', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [object[]]$Step, + [Parameter(Mandatory = $true, HelpMessage = 'The Full Output File Path', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string]$OutputFile, + [Alias('Silent')] + [switch]$EnableException + ) + + foreach ($instance in $sqlinstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $instance" + continue + } + + if (!$Job) { + # This is because jobname isn't yet required + Write-Message -Level Warning -Message "You must specify a job using the -Job parameter." + return + } + + foreach ($name in $Job) { + $currentJob = $server.JobServer.Jobs[$name] + + if ($Step) { + $steps = $currentJob.JobSteps | Where-Object Name -in $Step + + if (!$steps) { + Write-Message -Level Warning -Message "$Step didn't return any steps" + return + } + } + else { + if (($currentJob.JobSteps).Count -gt 1) { + Write-Message -Level Output -Message "Which Job Step do you wish to add output file to?" + $steps = $currentJob.JobSteps | Out-GridView -Title "Choose the Job Steps to add an output file to" -PassThru -Verbose + } + else { + $steps = $currentJob.JobSteps + } + } + + if (!$steps) { + $steps = $currentJob.JobSteps + } + + foreach ($jobstep in $steps) { + $currentoutputfile = $jobstep.OutputFileName + + Write-Message -Level Verbose -Message "Current Output File for $currentJob is $currentoutputfile" + Write-Message -Level Verbose -Message "Adding $OutputFile to $jobstep for $currentJob" + + try { + if ($Pscmdlet.ShouldProcess($jobstep, "Changing Output File from $currentoutputfile to $OutputFile")) { + $jobstep.OutputFileName = $OutputFile + $jobstep.Alter() + $jobstep.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Job = $currentJob.Name + JobStep = $jobstep.Name + OutputFileName = $currentoutputfile + } + } + } + catch { + Stop-Function -Message "Failed to add $OutputFile to $jobstep for $currentJob" -InnerErrorRecord $_ -Target $currentJob + } + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentJobStep { + <# +.SYNOPSIS +Set-DbaAgentJobStep updates a job step. + +.DESCRIPTION +Set-DbaAgentJobStep updates a job step in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. Can be null if the the job id is being used. + +.PARAMETER StepName +The name of the step. + +.PARAMETER NewName +The new name for the step in case it needs to be renamed. + +.PARAMETER SubSystem +The subsystem used by the SQL Server Agent service to execute command. +Allowed values 'ActiveScripting','AnalysisCommand','AnalysisQuery','CmdExec','Distribution','LogReader','Merge','PowerShell','QueueReader','Snapshot','Ssis','TransactSql' + +.PARAMETER Command +The commands to be executed by SQLServerAgent service through subsystem. + +.PARAMETER CmdExecSuccessCode +The value returned by a CmdExec subsystem command to indicate that command executed successfully. + +.PARAMETER OnSuccessAction +The action to perform if the step succeeds. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnSuccessStepId +The ID of the step in this job to execute if the step succeeds and OnSuccessAction is "GoToNextStep". + +.PARAMETER OnFailAction +The action to perform if the step fails. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnFailStepId +The ID of the step in this job to execute if the step fails and OnFailAction is "GoToNextStep". + +.PARAMETER Database +The name of the database in which to execute a Transact-SQL step. The default is 'master'. + +.PARAMETER DatabaseUser +The name of the user account to use when executing a Transact-SQL step. The default is 'sa'. + +.PARAMETER RetryAttempts +The number of retry attempts to use if this step fails. The default is 0. + +.PARAMETER RetryInterval +The amount of time in minutes between retry attempts. The default is 0. + +.PARAMETER OutputFileName +The name of the file in which the output of this step is saved. + +.PARAMETER Flag +Sets the flag(s) for the job step. + +Flag Description +---------------------------------------------------------------------------- +AppendAllCmdExecOutputToJobHistory Job history, including command output, is appended to the job history file. +AppendToJobHistory Job history is appended to the job history file. +AppendToLogFile Job history is appended to the SQL Server log file. +AppendToTableLog Job history is appended to a log table. +LogToTableWithOverwrite Job history is written to a log table, overwriting previous contents. +None Job history is not appended to a file. +ProvideStopProcessEvent Job processing is stopped. + +.PARAMETER ProxyName +The name of the proxy that the job step runs as. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJobStep + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -NewName Step2 +Changes the name of the step in "Job1" with the name Step1 to Step2 + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1, Job2 -StepName Step1 -Database msdb +Changes job steps in multiple jobs with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1, Job2 -StepName Step1 -Database msdb +Changes job steps in multiple jobs on multiple servers with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb for multiple servers + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentJobStep -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb for multiple servers using pipeline + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [Parameter(Mandatory = $false)] + [string]$NewName, + [Parameter(Mandatory = $false)] + [ValidateSet('ActiveScripting', 'AnalysisCommand', 'AnalysisQuery', 'CmdExec', 'Distribution', 'LogReader', 'Merge', 'PowerShell', 'QueueReader', 'Snapshot', 'Ssis', 'TransactSql')] + [string]$Subsystem, + [Parameter(Mandatory = $false)] + [string]$Command, + [Parameter(Mandatory = $false)] + [int]$CmdExecSuccessCode, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnSuccessAction, + [Parameter(Mandatory = $false)] + [int]$OnSuccessStepId, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnFailAction, + [Parameter(Mandatory = $false)] + [int]$OnFailStepId, + [Parameter(Mandatory = $false)] + [string]$Database, + [Parameter(Mandatory = $false)] + [string]$DatabaseUser, + [Parameter(Mandatory = $false)] + [int]$RetryAttempts, + [Parameter(Mandatory = $false)] + [int]$RetryInterval, + [Parameter(Mandatory = $false)] + [string]$OutputFileName, + [Parameter(Mandatory = $false)] + [ValidateSet('AppendAllCmdExecOutputToJobHistory', 'AppendToJobHistory', 'AppendToLogFile', 'LogToTableWithOverwrite', 'None', 'ProvideStopProcessEvent')] + [string[]]$Flag, + [Parameter(Mandatory = $false)] + [string]$ProxyName, + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + begin { + # Check the parameter on success step id + if (($OnSuccessAction -ne 'GoToStep') -and ($OnSuccessStepId -ge 1)) { + Stop-Function -Message "Parameter OnSuccessStepId can only be used with OnSuccessAction 'GoToStep'." -Target $SqlInstance + return + } + + # Check the parameter on success step id + if (($OnFailAction -ne 'GoToStep') -and ($OnFailStepId -ge 1)) { + Stop-Function -Message "Parameter OnFailStepId can only be used with OnFailAction 'GoToStep'." -Target $SqlInstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + Stop-Function -Message "Job $j doesn't exists on $instance" -Target $instance -Continue + } + else { + # Check if the job step exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + Stop-Function -Message "Step $StepName doesn't exists for job $j" -Target $instance -Continue + } + else { + + # Get the job step + $JobStep = $Server.JobServer.Jobs[$j].JobSteps[$StepName] + + Write-Message -Message "Modifying job $j on $instance" -Level Verbose + + #region job step options + # Setting the options for the job step + if ($NewName) { + Write-Message -Message "Setting job step name to $NewName" -Level Verbose + $JobStep.Rename($NewName) + } + + if ($Subsystem) { + Write-Message -Message "Setting job step subsystem to $Subsystem" -Level Verbose + $JobStep.Subsystem = $Subsystem + } + + if ($Command) { + Write-Message -Message "Setting job step command to $Command" -Level Verbose + $JobStep.Command = $Command + } + + if ($CmdExecSuccessCode) { + Write-Message -Message "Setting job step command exec success code to $CmdExecSuccessCode" -Level Verbose + $JobStep.CommandExecutionSuccessCode = $CmdExecSuccessCode + } + + if ($OnSuccessAction) { + Write-Message -Message "Setting job step success action to $OnSuccessAction" -Level Verbose + $JobStep.OnSuccessAction = $OnSuccessAction + } + + if ($OnSuccessStepId) { + Write-Message -Message "Setting job step success step id to $OnSuccessStepId" -Level Verbose + $JobStep.OnSuccessStep = $OnSuccessStepId + } + + if ($OnFailAction) { + Write-Message -Message "Setting job step fail action to $OnFailAction" -Level Verbose + $JobStep.OnFailAction = $OnFailAction + } + + if ($OnFailStepId) { + Write-Message -Message "Setting job step fail step id to $OnFailStepId" -Level Verbose + $JobStep.OnFailStep = $OnFailStepId + } + + if ($Database) { + # Check if the database is present on the server + if ($Server.Databases.Name -contains $Database) { + Write-Message -Message "Setting job step database name to $Database" -Level Verbose + $JobStep.DatabaseName = $Database + } + else { + Stop-Function -Message "The database is not present on instance $instance." -Target $instance -Continue + } + } + + if (($DatabaseUser) -and ($Database)) { + # Check if the username is present in the database + if ($Server.Databases[$Database].Users.Name -contains $DatabaseUser) { + Write-Message -Message "Setting job step database username to $DatabaseUser" -Level Verbose + $JobStep.DatabaseUserName = $DatabaseUser + } + else { + Stop-Function -Message "The database user is not present in the database $Database on instance $instance." -Target $instance -Continue + } + } + + if ($RetryAttempts) { + Write-Message -Message "Setting job step retry attempts to $RetryAttempts" -Level Verbose + $JobStep.RetryAttempts = $RetryAttempts + } + + if ($RetryInterval) { + Write-Message -Message "Setting job step retry interval to $RetryInterval" -Level Verbose + $JobStep.RetryInterval = $RetryInterval + } + + if ($OutputFileName) { + Write-Message -Message "Setting job step output file name to $OutputFileName" -Level Verbose + $JobStep.OutputFileName = $OutputFileName + } + + if ($ProxyName) { + # Check if the proxy exists + if ($Server.JobServer.ProxyAccounts.Name -contains $ProxyName) { + Write-Message -Message "Setting job step proxy name to $ProxyName" -Level Verbose + $JobStep.ProxyName = $ProxyName + } + else { + Stop-Function -Message "The proxy name $ProxyName doesn't exist on instance $instance." -Target $instance -Continue + } + } + + if ($Flag.Count -ge 1) { + Write-Message -Message "Setting job step flag(s) to $($Flags -join ',')" -Level Verbose + $JobStep.JobStepFlags = $Flag + } + #region job step options + + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Changing the job step $StepName for job $j")) { + try { + Write-Message -Message "Changing the job step $StepName for job $j" -Level Verbose + + # Change the job step + $JobStep.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the job step" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } + + } # foreach object job + } # foreach object intance + } # process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing job step(s)" -Level Verbose + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentSchedule { + <# +.SYNOPSIS +Set-DbaAgentSchedule updates a schedule in the msdb database. + +.DESCRIPTION +Set-DbaAgentSchedule will help update a schedule for a job. It does not attach the schedule to a job. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job that has the schedule. + +.PARAMETER ScheduleName +The name of the schedule. + +.PARAMETER NewName +The new name for the schedule. + +.PARAMETER Enabled +Set the schedule to enabled. + +.PARAMETER Disabled +Set the schedule to disabled. + +.PARAMETER FrequencyType +A value indicating when a job is to be executed. +Allowed values are 1, "Once", 4, "Daily", 8, "Weekly", 16, "Monthly", 32, "MonthlyRelative", 64, "AgentStart", 128 or "IdleComputer" + +.PARAMETER FrequencyInterval +The days that a job is executed +Allowed values are 1, "Sunday", 2, "Monday", 4, "Tuesday", 8, "Wednesday", 16, "Thursday", 32, "Friday", 64, "Saturday", 62, "Weekdays", 65, "Weekend", 127, "EveryDay". +If 62, "Weekdays", 65, "Weekend", 127, "EveryDay" is used it overwwrites any other value that has been passed before. + +.PARAMETER FrequencySubdayType +Specifies the units for the subday FrequencyInterval. +Allowed values are 1, "Time", 2, "Seconds", 4, "Minutes", 8 or "Hours" + +.PARAMETER FrequencySubdayInterval +The number of subday type periods to occur between each execution of a job. + +.PARAMETER FrequencySubdayInterval +The number of subday type periods to occur between each execution of a job. + +.PARAMETER FrequencyRelativeInterval +A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + +.PARAMETER FrequencyRecurrenceFactor +The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + +.PARAMETER StartDate +The date on which execution of a job can begin. + +.PARAMETER EndDate +The date on which execution of a job can stop. + +.PARAMETER StartTime +The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. +Example: '010000' for 01:00:00 AM. +Example: '140000' for 02:00:00 PM. + +.PARAMETER EndTime +The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. +Example: '010000' for 01:00:00 AM. +Example: '140000' for 02:00:00 PM. + +.PARAMETER Owner +The name of the server principal that owns the schedule. If no value is given the schedule is owned by the creator. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. +It will also remove the any present schedules with the same name for the specific job. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentSchedule + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1 -ScheduleName daily -Enabled +Changes the schedule for Job1 with the name 'daily' to enabled + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1 -ScheduleName daily -NewName weekly -FrequencyType Weekly -FrequencyInterval Monday, Wednesday, Friday +Changes the schedule for Job1 with the name daily to have a new name weekly + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1, Job2, Job3 -ScheduleName daily -StartTime '230000' +Changes the start time of the schedule for Job1 to 11 PM for multiple jobs + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1, sql2, sql3 -Job Job1 -ScheduleName daily -Enabled +Changes the schedule for Job1 with the name daily to enabled on multiple servers + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentSchedule -Job Job1 -ScheduleName 'daily' -Enabled +Changes the schedule for Job1 with the name 'daily' to enabled on multiple servers using pipe line + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [string]$ScheduleName, + [Parameter(Mandatory = $false)] + [string]$NewName, + [Parameter(Mandatory = $false)] + [switch]$Enabled, + [Parameter(Mandatory = $false)] + [switch]$Disabled, + [ValidateSet(1, "Once", 4, "Daily", 8, "Weekly", 16, "Monthly", 32, "MonthlyRelative", 64, "AgentStart", 128, "IdleComputer")] + [object]$FrequencyType, + [Parameter(Mandatory = $false)] + [object[]]$FrequencyInterval, + [Parameter(Mandatory = $false)] + [ValidateSet(1, "Time", 2, "Seconds", 4, "Minutes", 8, "Hours")] + [object]$FrequencySubdayType, + [Parameter(Mandatory = $false)] + [int]$FrequencySubdayInterval, + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$FrequencyRelativeInterval, + [Parameter(Mandatory = $false)] + [int]$FrequencyRecurrenceFactor, + [Parameter(Mandatory = $false)] + [string]$StartDate, + [Parameter(Mandatory = $false)] + [string]$EndDate, + [Parameter(Mandatory = $false)] + [string]$StartTime, + [Parameter(Mandatory = $false)] + [string]$EndTime, + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + begin { + + # Check of the FrequencyType value is of type string and set the integer value + if ($FrequencyType -notin 0, 1, 4, 8, 16, 32, 64, 128) { + [int]$FrequencyType = switch ($FrequencyType) { "Once" { 1 } "Daily" { 4 } "Weekly" { 8 } "Monthly" { 16 } "MonthlyRelative" { 32 } "AgentStart" { 64 } "IdleComputer" { 128 } } + } + + # Check of the FrequencySubdayType value is of type string and set the integer value + if ($FrequencySubdayType -notin 0, 1, 2, 4, 8) { + [int]$FrequencySubdayType = switch ($FrequencySubdayType) { "Time" { 1 } "Seconds" { 2 } "Minutes" { 4 } "Hours" { 8 } default {0} } + } + + # Check if the interval is valid + if (($FrequencyType -eq 4) -and ($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365)) { + Stop-Function -Message "The interval $FrequencyInterval needs to be higher than 1 and lower than 365 when using a daily frequency the interval." -Target $SqlInstance + return + } + + # Check if the recurrence factor is set for weekly or monthly interval + if (($FrequencyType -in 8, 16) -and $FrequencyRecurrenceFactor -lt 1) { + if ($Force) { + $FrequencyRecurrenceFactor = 1 + Write-Message -Message "Recurrence factor not set for weekly or monthly interval. Setting it to $FrequencyRecurrenceFactor." -Level Verbose + } + else { + Stop-Function -Message "The recurrence factor $FrequencyRecurrenceFactor needs to be at least on when using a weekly or monthly interval." -Target $SqlInstance + return + } + } + + # Check the subday interval + if (($FrequencySubdayType -in 2, 4) -and (-not ($FrequencySubdayInterval -ge 1 -or $FrequencySubdayInterval -le 59))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $SqlInstance + return + } + elseif (($FrequencySubdayType -eq 8) -and (-not ($FrequencySubdayInterval -ge 1 -and $FrequencySubdayInterval -le 23))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $SqlInstance + return + } + + # Check of the FrequencyInterval value is of type string and set the integer value + if (($null -ne $FrequencyType)) { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # If the FrequencyInterval is set for the weekly FrequencyType + if ($FrequencyType -eq 8) { + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 4 } + "Wednesday" { $Interval += 8 } + "Thursday" { $Interval += 16 } + "Friday" { $Interval += 32 } + "Saturday" { $Interval += 64 } + "Weekdays" { $Interval = 62 } + "Weekend" { $Interval = 65 } + "EveryDay" {$Interval = 127 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 4 { $Interval += 4 } + 8 { $Interval += 8 } + 16 { $Interval += 16 } + 31 { $Interval += 32 } + 64 { $Interval += 64 } + 62 { $Interval = 62 } + 65 { $Interval = 65 } + 127 {$Interval = 127 } + } + } + } + + # If the FrequencyInterval is set for the relative monthly FrequencyInterval + if ($FrequencyType -eq 32) { + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 3 } + "Wednesday" { $Interval += 4 } + "Thursday" { $Interval += 5 } + "Friday" { $Interval += 6 } + "Saturday" { $Interval += 7 } + "Day" { $Interval += 8 } + "Weekday" { $Interval += 9 } + "WeekendDay" { $Interval += 10 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 3 { $Interval += 3 } + 4 { $Interval += 4 } + 5 { $Interval += 5 } + 6 { $Interval += 6 } + 7 { $Interval += 7 } + 8 { $Interval += 8 } + 9 { $Interval += 9 } + 10 { $Interval += 10 } + } + } + } + } + + # Check of the relative FrequencyInterval value is of type string and set the integer value + if (($FrequencyRelativeInterval -notin 1, 2, 4, 8, 16) -and $null -ne $FrequencyRelativeInterval) { + [int]$FrequencyRelativeInterval = switch ($FrequencyRelativeInterval) { "First" { 1 } "Second" { 2 } "Third" { 4 } "Fourth" { 8 } "Last" { 16 } "Unused" { 0 } default { 0 }} + } + + # Check if the interval is valid + if (($FrequencyType -eq 4) -and ($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365)) { + Stop-Function -Message "The interval $FrequencyInterval needs to be higher than 1 and lower than 365 when using a daily frequency the interval." -Target $SqlInstance + return + } + + # Setup the regex + $RegexDate = '(? + [CmdletBinding(DefaultParameterSetName = 'Credential')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName = $env:COMPUTERNAME, + + [Parameter(ParameterSetName = "Credential")] + [PSCredential] + $Credential, + + [Parameter(ParameterSetName = "Windows")] + [switch] + $UseWindowsCredentials, + + [switch] + $OverrideExplicitCredential, + + [switch] + $OverrideConnectionPolicy, + + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType] + $DisabledConnectionTypes = 'None', + + [switch] + $DisableBadCredentialCache, + + [switch] + $DisableCimPersistence, + + [switch] + $DisableCredentialAutoRegister, + + [switch] + $EnableCredentialFailover, + + [Parameter(ParameterSetName = "Credential")] + [switch] + $WindowsCredentialsAreBad, + + [Microsoft.Management.Infrastructure.Options.WSManSessionOptions] + $CimWinRMOptions, + + [Microsoft.Management.Infrastructure.Options.DComSessionOptions] + $CimDCOMOptions, + + [System.Management.Automation.PSCredential[]] + $AddBadCredential, + + [System.Management.Automation.PSCredential[]] + $RemoveBadCredential, + + [switch] + $ClearBadCredential, + + [switch] + $ClearCredential, + + [switch] + $ResetCredential, + + [switch] + $ResetConnectionStatus, + + [switch] + $ResetConfiguration, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting execution" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $disable_cache = Get-DbaConfigValue -Name 'ComputerManagement.Cache.Disable.All' -Fallback $false + } + PROCESS { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Processing computer: $($connectionObject.Connection.ComputerName)" + + $connection = $connectionObject.Connection + + if ($ResetConfiguration) { + Write-Message -Level Verbose -Message "Resetting the configuration to system default" + + $connection.RestoreDefaultConfiguration() + } + + if ($ResetConnectionStatus) { + Write-Message -Level Verbose -Message "Resetting the connection status" + + $connection.CimRM = 'Unknown' + $connection.CimDCOM = 'Unknown' + $connection.Wmi = 'Unknown' + $connection.PowerShellRemoting = 'Unknown' + + $connection.LastCimRM = New-Object System.DateTime(0) + $connection.LastCimDCOM = New-Object System.DateTime(0) + $connection.LastWmi = New-Object System.DateTime(0) + $connection.LastPowerShellRemoting = New-Object System.DateTime(0) + } + + if ($ResetCredential) { + Write-Message -Level Verbose -Message "Resetting credentials" + + $connection.KnownBadCredentials.Clear() + $connection.Credentials = $null + $connection.UseWindowsCredentials = $false + $connection.WindowsCredentialsAreBad = $false + } + else { + if ($ClearBadCredential) { + Write-Message -Level Verbose -Message "Clearing bad credentials" + + $connection.KnownBadCredentials.Clear() + $connection.WindowsCredentialsAreBad = $false + } + + if ($ClearCredential) { + Write-Message -Level Verbose -Message "Clearing credentials" + + $connection.Credentials = $null + $connection.UseWindowsCredentials = $false + } + } + + foreach ($badCred in $RemoveBadCredential) { + $connection.RemoveBadCredential($badCred) + } + + foreach ($badCred in $AddBadCredential) { + $connection.AddBadCredential($badCred) + } + + if (Test-Bound "Credential") { $connection.Credentials = $Credential } + if ($UseWindowsCredentials) { + $connection.Credentials = $null + $connection.UseWindowsCredentials = $UseWindowsCredentials + } + if (Test-Bound "OverrideExplicitCredential") { $connection.OverrideExplicitCredential = $OverrideExplicitCredential } + if (Test-Bound "DisabledConnectionTypes") { $connection.DisabledConnectionTypes = $DisabledConnectionTypes } + if (Test-Bound "DisableBadCredentialCache") { $connection.DisableBadCredentialCache = $DisableBadCredentialCache } + if (Test-Bound "DisableCimPersistence") { $connection.DisableCimPersistence = $DisableCimPersistence } + if (Test-Bound "DisableCredentialAutoRegister") { $connection.DisableCredentialAutoRegister = $DisableCredentialAutoRegister } + if (Test-Bound "EnableCredentialFailover") { $connection.DisableCredentialAutoRegister = $EnableCredentialFailover } + if (Test-Bound "WindowsCredentialsAreBad") { $connection.WindowsCredentialsAreBad = $WindowsCredentialsAreBad } + if (Test-Bound "CimWinRMOptions") { $connection.CimWinRMOptions = $CimWinRMOptions } + if (Test-Bound "CimDCOMOptions") { $connection.CimDCOMOptions = $CimDCOMOptions } + if (Test-Bound "OverrideConnectionPolicy") { $connection.OverrideConnectionPolicy = $OverrideConnectionPolicy } + + if (-not $disable_cache) { + Write-Message -Level Verbose -Message "Writing connection to cache" + [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$connectionObject.Connection.ComputerName] = $connection + } + else { Write-Message -Level Verbose -Message "Skipping writing to cache, since the cache has been disabled!" } + $connection + } + } + END { + Write-Message -Level InternalComment -Message "Stopping execution" + } +} +function Set-DbaConfig { + <# + .SYNOPSIS + Sets configuration entries. + + .DESCRIPTION + This function creates or changes configuration values. + These are used in dbatools to provide dynamic configuration information outside the PowerShell variable system. + + .PARAMETER FullName + The full name of a configuration element. Must be namespaced .. + The name can have any number of sub-segments, in order to better group configurations thematically. + + .PARAMETER Name + Name of the configuration entry. If an entry of exactly this non-casesensitive name already exists, its value will be overwritten. + Duplicate names across different modules are possible and will be treated separately. + If a name contains namespace notation and no module is set, the first namespace element will be used as module instead of name. Example: + -Name "Nordwind.Server" + Is Equivalent to + -Name "Server" -Module "Nordwind" + + .PARAMETER Module + This allows grouping configuration elements into groups based on the module/component they server. + If this parameter is not set, the configuration element is stored under its name only, which increases the likelyhood of name conflicts in large environments. + + .PARAMETER Value + The value to assign to the named configuration element. + + .PARAMETER Description + Using this, the configuration setting is given a description, making it easier for a user to comprehend, what a specific setting is for. + + .PARAMETER Validation + The name of the validation script used for input validation. + These can be used to validate make sure that input is of the proper data type. + New validation scripts can be registered using Register-PSFConfigValidation + + .PARAMETER Handler + A scriptblock that is executed when a value is being set. + Is only executed if the validation was successful (assuming there was a validation, of course) + + .PARAMETER Hidden + Setting this parameter hides the configuration from casual discovery. Configurations with this set will only be returned by Get-Config, if the parameter "-Force" is used. + This should be set for all system settings a user should have no business changing (e.g. for Infrastructure related settings such as mail server). + + .PARAMETER Default + Setting this parameter causes the system to treat this configuration as a default setting. If the configuration already exists, no changes will be performed. + Useful in scenarios where for some reason it is not practical to automatically set defaults before loading userprofiles. + + .PARAMETER Initialize + Use this when setting configurations as part of module import. + When initializing a configuration, it will only do a thing if the configuration hasn't already been initialized (So if you load the module multiple times or in multiple runspaces, it won't make a difference) + Also, if there already was a non-initialized setting set for a given configuration, it will then try to set the old value again. + This value will be processed by handlers, if any are set. + + .PARAMETER DisableValidation + This parameters disables the input validation - if any - when processing a setting. + Normally this shouldn't be circumvented, but just in case, it can be disabled. + + .PARAMETER DisableHandler + Internal Use Only. + This parameter disables the configuration handlers. + Configuration handlers are designed to automatically validate and process input set to a config value, in addition to writing the value. + In many cases, this is used to improve performance, by forking the value location also to a static C#-field, which is then used, rather than searching a Hashtable. + Sometimes it may only be used to introduce input validation. + During module import, some handlers are registered and many values written to configuration. + However, some of those values actually are already set as default values within the library. Processing a handler will cost a few ms. + Add up a couple dozen such events and the delay is very notable. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Config, Module + Author: Friedrich Weinmann + + .EXAMPLE + PS C:\> Set-DbaConfig -Name 'User' -Value "Friedrich" -Description "The user under which the show must go on." + + Creates a configuration entry named "User" with the value "Friedrich" + + .EXAMPLE + PS C:\> Set-DbaConfig -Name 'mymodule.User' -Value "Friedrich" -Description "The user under which the show must go on." -Handler $scriptBlock -Initialize -Validation String + + Creates a configuration entry ... + - Named "mymodule.user" + - With the value "Friedrich" + - It adds a description as noted + - It registers the scriptblock stored in $scriptBlock as handler + - It initializes the script. This block only executes the first time a it is run like this. Subsequent calls will be ignored. + - It registers the basic string input type validator + This is the default example for modules using the configuration system. + Note: While the -Handler parameter is optional, it is important to add it at the initial initialize call, if you are planning to add it. + Only then will the system validate previous settings (such as what a user might have placed in his user profile) + + .EXAMPLE + PS C:\> Set-DbaConfig 'ConfigLink' 'https://www.example.com/config.xml' 'Company' -Hidden + + Creates a configuration entry named "ConfigLink" in the "Company" module with the value 'https://www.example.com/config.xml'. + This entry is hidden from casual discovery using Get-Config. + + .EXAMPLE + PS C:\> Set-DbaConfig 'Network.Firewall' '10.0.0.2' -Default + + Creates a configuration entry named "Firewall" in the "Network" module with the value '10.0.0.2' + This is only set, if the setting does not exist yet. If it does, this command will apply no changes. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding(DefaultParameterSetName = "FullName")] + Param ( + [Parameter(ParameterSetName = "FullName", Position = 0, Mandatory = $true)] + [string] + $FullName, + + [Parameter(ParameterSetName = "Module", Position = 1, Mandatory = $true)] + [string] + $Name, + + [Parameter(ParameterSetName = "Module", Position = 0)] + [string] + $Module, + + [Parameter(ParameterSetName = "FullName", Position = 1)] + [Parameter(ParameterSetName = "Module", Position = 2)] + [AllowNull()] + [AllowEmptyCollection()] + [AllowEmptyString()] + $Value, + + [string] + $Description, + + [string] + $Validation, + + [System.Management.Automation.ScriptBlock] + $Handler, + + [switch] + $Hidden, + + [switch] + $Default, + + [switch] + $Initialize, + + [switch] + $DisableValidation, + + [switch] + $DisableHandler, + + [switch] + $EnableException + ) + + #region Prepare Names + if ($PSCmdlet.ParameterSetName -eq "FullName") { + if (-not $FullName.Trim(".").Contains(".")) { + Stop-Function -Message "Invalid Name: $FullName ! At least one '.' is required, to separate module from name" -EnableException $EnableException -Category InvalidArgument + return + } + + $Module = $FullName.Split(".")[0].ToLower().Trim(".") + $Name = $FullName.Substring(($Module.Length + 1)).ToLower().Trim(".") + $internalFullName = $FullName.ToLower().Trim(".") + } + else { + $Name = $Name.ToLower().Trim(".") + if ($Module) { $Module = $Module.ToLower().Trim(".") } + + if ((Test-Bound -ParameterName "Module" -Not) -and ($Name -match ".+\..+")) { + $r = $Name | select-string "^(.+?)\..+" -AllMatches + $Module = $r.Matches[0].Groups[1].Value + $Name = $Name.Substring($Module.Length + 1) + } + elseif ((Test-Bound -ParameterName "Module" -Not) -and ($Name -notmatch ".+\..+")) { + Stop-Function -Message "Invalid Name: $Name ! At least one '.' is required when not explicitly specifying a module name, to separate module from name" -EnableException $EnableException -Category InvalidArgument + return + } + + If ($Module) { $internalFullName = $Module, $Name -join "." } + else { $internalFullName = $Name } + } + #endregion Prepare Names + + #region Prepare runtime and kill execution as needed + if ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.ContainsKey($internalFullName)) { + $itExists = $true + $itIsInitialized = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Initialized + $itIsEnforced = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].PolicyEnforced + } + else { + $itExists = $false + $itIsInitialized = $false + $itIsEnforced = $false + } + + if ($itExists -and $Default) { return } + if ($itIsInitialized -and $Initialize) { return } + if ($itIsEnforced -and (-not $Initialize)) { + Stop-Function -Message "Could not update configuration due to policy settings: $internalFullName" -EnableException $EnableException -Category PermissionDenied + return + } + + if (Test-Bound -ParameterName "Validation") { + if (-not ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation.Keys -contains $Validation.ToLower())) { + Stop-Function -Message "Invalid validation name: $Validation. Supported validations: $([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation.Keys -join ", ")" -Category InvalidArgument -Target $Name + return + } + } + #endregion Prepare runtime and kill execution as needed + + #region Initializing a configuration + if ($Initialize) { + if ($itExists) { + $oldValue = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Value + $cfg = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] + } + else { $cfg = New-Object Sqlcollaborative.Dbatools.Configuration.Config } + $cfg.Name = $Name + $cfg.Module = $Module + $cfg.Description = $Description + $cfg.Value = $Value + $cfg.Handler = $Handler + if ($Validation) { $cfg.Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + $cfg.Hidden = $Hidden + $cfg.Initialized = $true + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] = $cfg + + if ($itExists) { Set-DbaConfig -Name $internalFullName -Value $oldValue } + } + #endregion Initializing a configuration + + #region Regular configuration update + else { + if (-not $itExists) { + $cfg = New-Object Sqlcollaborative.Dbatools.Configuration.Config + $cfg.Name = $Name + $cfg.Module = $Module + $cfg.Description = $Description + $cfg.Handler = $Handler + if ($Validation) { $cfg.Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + $cfg.Hidden = $Hidden + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] = $cfg + + Set-DbaConfig -Name $internalFullName -Value $Value + return + } + + else { + [Sqlcollaborative.Dbatools.Configuration.Config]$cfg = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] + if ((-not $DisableValidation) -and ($cfg.Validation) -and (Test-Bound -ParameterName "Value")) { + $testResult = [scriptblock]::Create($cfg.Validation.ToString()).Invoke($Value) + if (-not $TestResult.Success) { + Stop-Function -Message "Could not update configuration $internalFullName | Failed validation: $($testResult.Message)" -EnableException $EnableException -Category InvalidResult -Target $internalFullName + return + } + $Value = $testResult.Value + } + + if (Test-Bound -ParameterName "Hidden") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Hidden = $Hidden } + if (Test-Bound -ParameterName "Value") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Value = $Value } + if (Test-Bound -ParameterName "Description") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Description = $Description } + if (Test-Bound -ParameterName "Handler") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Handler = $Handler } + if (Test-Bound -ParameterName "Validation") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + + if ((-not $DisableHandler) -and ($cfg.Handler) -and (Test-Bound -ParameterName "Value")) { + try { [scriptblock]::Create($cfg.Handler.ToString()).Invoke($Value) } + catch { + Stop-Function -Message "Could not update configuration $internalFullName | Failed handling $_" -EnableException $EnableException -Category InvalidResult -Target $internalFullName + return + } + } + } + } + #endregion Regular configuration update +} +function Set-DbaDatabaseOwner { + <# + .SYNOPSIS + Sets database owners with a desired login if databases do not match that owner. + + .DESCRIPTION + This function will alter database ownership to match a specified login if their current owner does not match the target login. By default, the target login will be 'sa', but the function will allow the user to specify a different login for ownership. The user can also apply this to all databases or only to a select list of databases (passed as either a comma separated list or a string array). + + Best Practice reference: http://weblogs.sqlteam.com/dang/archive/2008/01/13/Database-Owner-Troubles.aspx + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER TargetLogin + Specifies the login that you wish check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. This must be a valid security principal which exists on the target server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Owner, DbOwner + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDatabaseOwner + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance localhost + + Sets database owner to 'sa' on all databases where the owner does not match 'sa'. + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance localhost -TargetLogin DOMAIN\account + + Sets the database owner to DOMAIN\account on all databases where the owner does not match DOMAIN\account. + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance sqlserver -Database db1, db2 + + Sets database owner to 'sa' on the db1 and db2 databases if their current owner does not match 'sa'. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias("Login")] + [string]$TargetLogin, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance." + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # dynamic sa name for orgs who have changed their sa name + if (!$TargetLogin) { + $TargetLogin = ($server.logins | Where-Object { $_.id -eq 1 }).Name + } + + #Validate login + if (($server.Logins.Name) -notcontains $TargetLogin) { + Stop-Function -Message "$TargetLogin is not a valid login on $instance. Moving on." -Continue -EnableException $EnableException + } + + #Owner cannot be a group + $TargetLoginObject = $server.Logins | where-object {$PSItem.Name -eq $TargetLogin }| Select-Object -property Name, LoginType + if ($TargetLoginObject.LoginType -eq 'WindowsGroup') { + Stop-Function -Message "$TargetLogin is a group, therefore can't be set as owner. Moving on." -Continue -EnableException $EnableException + } + + #Get database list. If value for -Database is passed, massage to make it a string array. + #Otherwise, use all databases on the instance where owner not equal to -TargetLogin + #use where owner and target login do not match + #exclude system dbs + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.Owner -ne $TargetLogin -and @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name} + + #filter collection based on -Databases/-Exclude parameters + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + + Write-Message -Level Verbose -Message "Updating $($dbs.Count) database(s)." + foreach ($db in $dbs) { + $dbname = $db.name + if ($PSCmdlet.ShouldProcess($instance, "Setting database owner for $dbname to $TargetLogin")) { + try { + Write-Message -Level Verbose -Message "Setting database owner for $dbname to $TargetLogin on $instance." + # Set database owner to $TargetLogin (default 'sa') + # Ownership validations checks + + #Database is online and accessible + if ($db.Status -notmatch 'Normal') { + Write-Message -Level Warning -Message "$dbname on $instance is in a $($db.Status) state and can not be altered. It will be skipped." + } + #Database is updatable, not read-only + elseif ($db.IsUpdateable -eq $false) { + Write-Message -Level Warning -Message "$dbname on $instance is not in an updateable state and can not be altered. It will be skipped." + } + #Is the login mapped as a user? Logins already mapped in the database can not be the owner + elseif ($db.Users.name -contains $TargetLogin) { + Write-Message -Level Warning -Message "$dbname on $instance has $TargetLogin as a mapped user. Mapped users can not be database owners." + } + else { + $db.SetOwner($TargetLogin) + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + Owner = $TargetLogin + } + } + } + catch { + Stop-Function -Message "Failure updating owner." -ErrorRecord $_ -Target $instance -Continue + } + } + } + } + } +} + +#ValidationTags#Messaging,FlowControl,Pipeline# +function Set-DbaDatabaseState { + <# + .SYNOPSIS + Sets various options for databases, hereby called "states" + + .DESCRIPTION + Sets some common "states" on databases: + - "RW" options (ReadOnly, ReadWrite) + - "Status" options (Online, Offline, Emergency, plus a special "Detached") + - "Access" options (SingleUser, RestrictedUser, MultiUser) + + Returns an object with SqlInstance, Database, RW, Status, Access, Notes + + Notes gets filled when something went wrong setting the state + + .PARAMETER SqlInstance + The SQL Server that you're connecting to + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. if unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllDatabases + This is a parameter that was included for safety, so you don't accidentally set options on all databases without specifying + + .PARAMETER ReadOnly + RW Option : Sets the database as READ_ONLY + + .PARAMETER ReadWrite + RW Option : Sets the database as READ_WRITE + + .PARAMETER Online + Status Option : Sets the database as ONLINE + + .PARAMETER Offline + Status Option : Sets the database as OFFLINE + + .PARAMETER Emergency + Status Option : Sets the database as EMERGENCY + + .PARAMETER Detached + Status Option : Detaches the database + + .PARAMETER SingleUser + Access Option : Sets the database as SINGLE_USER + + .PARAMETER RestrictedUser + Access Option : Sets the database as RESTRICTED_USER + + .PARAMETER MultiUser + Access Option : Sets the database as MULTI_USER + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + For most options, this translates to instantly rolling back any open transactions + that may be stopping the process. + For -Detached it is required to break mirroring and Availability Groups + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER InputObject + Accepts piped database objects + + .NOTES + Tags: Database, State + Author: niphlod + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDatabaseState + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -Offline + + Sets the HR database as OFFLINE + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -AllDatabases -Exclude HR -Readonly -Force + + Sets all databases of the sqlserver2014a instance, except for HR, as READ_ONLY + + .EXAMPLE + Get-DbaDatabaseState -SqlInstance sql2016 | Where-Object Status -eq 'Offline' | Set-DbaDatabaseState -Online + + Finds all offline databases and sets them to online + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -SingleUser + + Sets the HR database as SINGLE_USER + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -SingleUser -Force + + Sets the HR database as SINGLE_USER, dropping all other connections (and rolling back open transactions) + + .EXAMPLE + Get-DbaDatabase -SqlInstance sqlserver2014a -Database HR | Set-DbaDatabaseState -SingleUser -Force + + Gets the databases from Get-DbaDatabase, and sets them as SINGLE_USER, dropping all other connections (and rolling back open transactions) + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Server")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [switch]$ReadOnly, + [switch]$ReadWrite, + [switch]$Online, + [switch]$Offline, + [switch]$Emergency, + [switch]$Detached, + [switch]$SingleUser, + [switch]$RestrictedUser, + [switch]$MultiUser, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException, + [parameter(Mandatory = $true, ValueFromPipeline, ParameterSetName = "Database")] + [PsCustomObject[]]$InputObject + ) + + begin { + function Get-WrongCombo($optset, $allparams) { + $x = 0 + foreach ($opt in $optset) { + if ($allparams.ContainsKey($opt)) { $x += 1 } + } + if ($x -gt 1) { + $msg = $optset -Join ',-' + $msg = "You can only specify one of: -" + $msg + throw $msg + } + } + + function Edit-DatabaseState($sqlinstance, $dbname, $opt, $immediate = $false) { + $warn = $null + $sql = "ALTER DATABASE [$dbname] SET $opt" + if ($immediate) { + $sql += " WITH ROLLBACK IMMEDIATE" + } + else { + $sql += " WITH NO_WAIT" + } + try { + Write-Message -Level System -Message $sql + if ($immediate) { + # this can be helpful only for SINGLE_USER databases + # but since $immediate is called, it does no more harm + # than the immediate rollback + $sqlinstance.KillAllProcesses($dbname) + } + $null = $sqlinstance.Query($sql) + } + catch { + $warn = "Failed to set '$dbname' to $opt" + Write-Message -Level Warning -Message $warn + } + return $warn + } + + $StatusHash = @{ + 'Offline' = 'OFFLINE' + 'Normal' = 'ONLINE' + 'EmergencyMode' = 'EMERGENCY' + } + + function Get-DbState($databaseName, $dbStatuses) { + $base = $dbStatuses | Where-Object DatabaseName -ceq $databaseName + foreach ($status in $StatusHash.Keys) { + if ($base.Status -match $status) { + $base.Status = $StatusHash[$status] + break + } + } + return $base + } + + $RWExclusive = @('ReadOnly', 'ReadWrite') + $StatusExclusive = @('Online', 'Offline', 'Emergency', 'Detached') + $AccessExclusive = @('SingleUser', 'RestrictedUser', 'MultiUser') + $allparams = $PSBoundParameters + try { + Get-WrongCombo -optset $RWExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + try { + Get-WrongCombo -optset $StatusExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + try { + Get-WrongCombo -optset $AccessExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + } + process { + if (Test-FunctionInterrupt) { return } + $dbs = @() + if (!$Database -and !$AllDatabases -and !$InputObject -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify a -AllDatabases or -Database to continue" + return + } + + if ($InputObject) { + if ($InputObject.Database) { + # comes from Get-DbaDatabaseState + $dbs += $InputObject.Database + } + elseif ($InputObject.Name) { + # comes from Get-DbaDatabase + $dbs += $InputObject + } + } + else { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $all_dbs = $server.Databases + $dbs += $all_dbs | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } + + if ($database) { + $dbs = $dbs | Where-Object { $database -contains $_.Name } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + } + + # need to pick up here + foreach ($db in $dbs) { + if ($db.Name -in @('master', 'model', 'msdb', 'tempdb', 'distribution')) { + Write-Message -Level Warning -Message "Database $db is a system one, skipping" + Continue + } + $dbStatuses = @{} + $server = $db.Parent + if ($server -notin $dbStatuses.Keys) { + $dbStatuses[$server] = Get-DbaDatabaseState -SqlInstance $server + } + + # normalizing properties returned by SMO to something more "fixed" + $db_status = Get-DbState -DatabaseName $db.Name -dbStatuses $dbStatuses[$server] + + + $warn = @() + + if ($db.DatabaseSnapshotBaseName.Length -gt 0) { + Write-Message -Level Warning -Message "Database $db is a snapshot, skipping" + Continue + } + + if ($ReadOnly -eq $true) { + if ($db_status.RW -eq 'READ_ONLY') { + Write-Message -Level VeryVerbose -Message "Database $db is already READ_ONLY" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to READ_ONLY")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to READ_ONLY" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_ONLY" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.RW = 'READ_ONLY' + } + } + } + } + + if ($ReadWrite -eq $true) { + if ($db_status.RW -eq 'READ_WRITE') { + Write-Message -Level VeryVerbose -Message "Database $db is already READ_WRITE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to READ_WRITE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to READ_WRITE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_WRITE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.RW = 'READ_WRITE' + } + } + } + } + + if ($Online -eq $true) { + if ($db_status.Status -eq 'ONLINE') { + Write-Message -Level VeryVerbose -Message "Database $db is already ONLINE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to ONLINE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to ONLINE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "ONLINE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.Status = 'ONLINE' + } + } + } + } + + if ($Offline -eq $true) { + if ($db_status.Status -eq 'OFFLINE') { + Write-Message -Level VeryVerbose -Message "Database $db is already OFFLINE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to OFFLINE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to OFFLINE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.Status = 'OFFLINE' + } + } + } + } + + if ($Emergency -eq $true) { + if ($db_status.Status -eq 'EMERGENCY') { + Write-Message -Level VeryVerbose -Message "Database $db is already EMERGENCY" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to EMERGENCY")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to EMERGENCY" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "EMERGENCY" -immediate $Force + if (!$partial) { + $db_status.Status = 'EMERGENCY' + } + } + } + } + + if ($SingleUser -eq $true) { + if ($db_status.Access -eq 'SINGLE_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already SINGLE_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to SINGLE_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to SINGLE_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "SINGLE_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'SINGLE_USER' + } + } + } + } + + if ($RestrictedUser -eq $true) { + if ($db_status.Access -eq 'RESTRICTED_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already RESTRICTED_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to RESTRICTED_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to RESTRICTED_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "RESTRICTED_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'RESTRICTED_USER' + } + } + } + } + + if ($MultiUser -eq $true) { + if ($db_status.Access -eq 'MULTI_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already MULTI_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to MULTI_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to MULTI_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "MULTI_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'MULTI_USER' + } + } + } + } + + if ($Detached -eq $true) { + # Refresh info about database state here (before detaching) + $db.Refresh() + # we need to see what snaps are on the server, as base databases cannot be dropped + $snaps = $server.Databases | Where-Object { $_.DatabaseSnapshotBaseName.Length -gt 0 } + $snaps = $snaps.DatabaseSnapshotBaseName | Get-Unique + if ($db.Name -in $snaps) { + Write-Message -Level Warning -Message "Database $db has snapshots, you need to drop them before detaching, skipping..." + Continue + } + if ($db.IsMirroringEnabled -eq $true -or $db.AvailabilityGroupName.Length -gt 0) { + if ($Force -eq $false) { + Write-Message -Level Warning -Message "Needs -Force to detach $db, skipping" + Continue + } + } + + if ($db.IsMirroringEnabled) { + if ($Pscmdlet.ShouldProcess($server, "Break mirroring for $db")) { + try { + $db.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $db.Alter() + $db.Refresh() + Write-Message -Level VeryVerbose -Message "Broke mirroring for $db" + } + catch { + Stop-Function -Message "Could not break mirror for $db. Skipping." -ErrorRecord $_ -Target $server -Continue + } + } + } + + if ($db.AvailabilityGroupName) { + $agname = $db.AvailabilityGroupName + if ($Pscmdlet.ShouldProcess($server, "Removing $db from AG [$agname]")) { + try { + $server.AvailabilityGroups[$db.AvailabilityGroupName].AvailabilityDatabases[$db.Name].Drop() + Write-Message -Level VeryVerbose -Message "Successfully removed $db from AG [$agname] on $server" + } + catch { + Stop-Function -Message "Could not remove $db from AG [$agname] on $server" -ErrorRecord $_ -Target $server -Continue + } + } + } + + # DBA 101 should encourage detaching just OFFLINE databases + # we can do that here + if ($Pscmdlet.ShouldProcess($server, "Detaching $db")) { + if ($db_status.Status -ne 'OFFLINE') { + $null = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $true + } + try { + $sql = "EXEC master.dbo.sp_detach_db N'$($db.Name)'" + Write-Message -Level System -Message $sql + $null = $server.Query($sql) + $db_status.Status = 'DETACHED' + } + catch { + Stop-Function -Message "Failed to detach $db" -ErrorRecord $_ -Target $server -Continue + $warn += "Failed to detach" + } + + } + + } + if ($warn) { + $warn = $warn | Get-Unique + $warn = $warn -Join ';' + } + else { + $warn = $null + } + if ($Detached -eq $true) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + RW = $db_status.RW + Status = $db_status.Status + Access = $db_status.Access + Notes = $warn + Database = $db + } | Select-DefaultView -ExcludeProperty Database + } + else { + $db.Refresh() + if ($null -eq $warn) { + # we avoid reenumerating properties + $newstate = $db_status + } + else { + $newstate = Get-DbState -databaseName $db.Name -dbStatuses $stateCache[$server] + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + RW = $newstate.RW + Status = $newstate.Status + Access = $newstate.Access + Notes = $warn + Database = $db + } | Select-DefaultView -ExcludeProperty Database + } + } + + } + + end { + + } +} +function Set-DbaDbCompression { + <# + .SYNOPSIS + Sets tables and indexes with preferred compression setting. + + .DESCRIPTION + This function sets the appropriate compression recommendation, determined either by using the Tiger Team's query or set to the CompressionType parameter. + + Remember Uptime is critical for the Tiger Team query, the longer uptime, the more accurate the analysis is. + You would probably be best if you utilized Get-DbaUptime first, before running this command. + + Set-DbaDbCompression script derived from GitHub and the tigertoolbox + (https://github.com/Microsoft/tigertoolbox/tree/master/Evaluate-Compression-Gains) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server. + + .PARAMETER CompressionType + Control the compression type applied. Default is 'Recommended' which uses the Tiger Team query to use the most appropriate setting per object. Other option is to compress all objects to either Row or Page. + + .PARAMETER MaxRunTime + Will continue to alter tables and indexes for the given amount of minutes. + + .PARAMETER PercentCompression + Will only work on the tables/indexes that have the calculated savings at and higher for the given number provided. + + .PARAMETER InputObject + Takes the output of Test-DbaDbCompression as an object and applied compression based on those recommendations. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Jason Squires (@js_0505, jstexasdba@gmail.com) + Tags: Compression, Table, Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDbCompression + + .EXAMPLE + Set-DbaDbCompression -SqlInstance localhost -MaxRunTime 60 -PercentCompression 25 + + Set the compression run time to 60 minutes and will start the compression of tables/indexes that have a difference of 25% or higher between current and recommended. + + .EXAMPLE + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -CompressionType Page + + Utilizes Page compression for all objects in DBName on ServerA with no time limit. + + .EXAMPLE + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -PercentCompression 25 | Out-GridView + + Will compress tables/indexes within the specified database that would show any % improvement with compression and with no time limit. The results will be piped into a nicely formated GridView. + + .EXAMPLE + $testCompression = Test-DbaDbCompression -SqlInstance ServerA -Database DBName + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -InputObject $testCompression + + Gets the compression suggestions from Test-DbaDbCompression into a variable, this can then be reviewed and passed into Set-DbaDbCompression. + + .EXAMPLE + $cred = Get-Credential sqladmin + Set-DbaDbCompression -SqlInstance ServerA -ExcludeDatabase Database -SqlCredential $cred -MaxRunTime 60 -PercentCompression 25 + + Set the compression run time to 60 minutes and will start the compression of tables/indexes for all databases except the specified excluded database. Only objects that have a difference of 25% or higher between current and recommended will be compressed. + + .EXAMPLE + $servers = 'Server1','Server2' + foreach ($svr in $servers) + { + Set-DbaDbCompression -SqlInstance $svr -MaxRunTime 60 -PercentCompression 25 | Export-Csv -Path C:\temp\CompressionAnalysisPAC.csv -Append + } + + Set the compression run time to 60 minutes and will start the compression of tables/indexes across all listed servers that have a difference of 25% or higher between current and recommended. Output of command is exported to a csv. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [ValidateSet("Recommended", "Page", "Row", "None")]$CompressionType = "Recommended", + [int]$MaxRunTime = 0, + [int]$PercentCompression = 0, + $InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + $starttime = Get-Date + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failed to process Instance $instance" -ErrorRecord $_ -Target $instance -Continue + } + + $Server.ConnectionContext.StatementTimeout = 0 + + #The reason why we do this is because of SQL 2016 and they now allow for compression on standard edition. + if ($server.EngineEdition -notmatch 'Enterprise' -and $server.VersionMajor -lt '13') { + Stop-Function -Message "Only SQL Server Enterprise Edition supports compression on $server" -Target $server -Continue + } + try { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.IsSystemObject -eq 0} + if ($Database) { + $dbs = $dbs | Where-Object { $_.Name -in $Database } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $_.Name -NotIn $ExcludeDatabase } + } + } + catch { + Stop-Function -Message "Unable to gather list of databases for $instance" -Target $instance -ErrorRecord $_ -Continue + } + + foreach ($db in $dbs) { + try { + Write-Message -Level Verbose -Message "Querying $instance - $db" + if ($db.status -ne 'Normal' -or $db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "$db is not accessible" -Target $db + continue + } + if ($db.CompatibilityLevel -lt 'Version100') { + Stop-Function -Message "$db has a compatibility level lower than Version100 and will be skipped." -Target $db -Continue + } + if ($CompressionType -eq "Recommended") { + if (Test-Bound "InputObject") { + Write-Message -Level Verbose -Message "Using passed in compression suggestions" + $compressionSuggestion = $InputObject | Where-Object {$_.Database -eq $db.name} + } + else { + Write-Message -Level Verbose -Message "Testing database for compression suggestions for $instance.$db" + $compressionSuggestion = Test-DbaDbCompression -SqlInstance $server -Database $db.Name + } + } + } + catch { + Stop-Function -Message "Unable to query $instance - $db" -Target $db -ErrorRecord $_ -Continue + } + + try { + if ($CompressionType -eq "Recommended") { + if ($Pscmdlet.ShouldProcess($db, "Applying suggested compression using results from Test-DbaDbCompression")) { + Write-Message -Level Verbose -Message "Applying suggested compression settings using Test-DbaDbCompression" + $results += $compressionSuggestion | Select-Object *, @{l = 'AlreadyProcesssed'; e = {"False"}} + foreach ($obj in ($results | Where-Object {$_.CompressionTypeRecommendation -ne 'NO_GAIN' -and $_.PercentCompression -ge $PercentCompression} | Sort-Object PercentCompression -Descending)) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + if ($obj.indexId -le 1) { + ##heaps and clustered indexes + Write-Message -Level Verbose -Message "Applying $($obj.CompressionTypeRecommendation) compression to $($obj.Database).$($obj.Schema).$($obj.TableName)" + $($server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $obj.Partition}).DataCompression = $($obj.CompressionTypeRecommendation) + $server.Databases[$obj.Database].Tables[$obj.TableName, $($obj.Schema)].Rebuild() + $obj.AlreadyProcesssed = "True" + } + else { + ##nonclustered indexes + Write-Message -Level Verbose -Message "Applying $($obj.CompressionTypeRecommendation) compression to $($obj.Database).$($obj.Schema).$($obj.TableName).$($obj.IndexName)" + $($server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].Indexes[$obj.IndexName].PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $obj.Partition}).DataCompression = $($obj.CompressionTypeRecommendation) + $server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].Indexes[$obj.IndexName].Rebuild() + $obj.AlreadyProcesssed = "True" + } + $obj + } + } + } + else { + if ($Pscmdlet.ShouldProcess($db, "Applying $CompressionType compression")) { + Write-Message -Level Verbose -Message "Applying $CompressionType compression to all objects in $($db.name)" + foreach ($obj in $server.Databases[$($db.name)].Tables | Where-Object {!$_.IsMemoryOptimized}) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + foreach ($p in $($obj.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing table $($obj.Schema).$($obj.Name)" + $($obj.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $obj.Rebuild() + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $null + Partition = $p.PartitionNumber + IndexID = 0 + IndexType = Switch ($obj.HasHeapIndex) {$false {"ClusteredIndex"} $true {"Heap"}} + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + + foreach ($index in $($obj.Indexes | Where-Object {!$_.IsMemoryOptimized})) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + foreach ($p in $($index.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing $($Index.IndexType) $($Index.Name) Partition $($p.PartitionNumber)" + + ## There is a bug in SMO where setting compression to None at the index level doesn't work + ## Once this UserVoice item is fixed the workaround can be removed + ## https://feedback.azure.com/forums/908035-sql-server/suggestions/34080112-data-compression-smo-bug + if ($CompressionType -eq "None") { + $query = "ALTER INDEX [$($index.Name)] ON $($index.Parent) REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = $CompressionType)" + $Server.Query($query, $db.Name) + } + else { + $($Index.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $index.Rebuild() + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $index.Name + Partition = $p.PartitionNumber + IndexID = $index.Id + IndexType = $index.IndexType + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + } + } + foreach ($index in $($server.Databases[$($db.name)].Views | Where-Object {$_.Indexes}).Indexes) { + foreach ($p in $($index.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing $($index.IndexType) $($index.Name) Partition $($p.PartitionNumber)" + + ## There is a bug in SMO where setting compression to None at the index level doesn't work + ## Once this UserVoice item is fixed the workaround can be removed + ## https://feedback.azure.com/forums/908035-sql-server/suggestions/34080112-data-compression-smo-bug + if ($CompressionType -eq "None") { + $query = "ALTER INDEX [$($index.Name)] ON $($index.Parent) REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = $CompressionType)" + $query + $Server.Query($query, $db.Name) + } + else { + $($index.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $index.Rebuild() + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $index.Name + Partition = $p.PartitionNumber + IndexID = $index.Id + IndexType = $index.IndexType + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + } + } + } + } + catch { + Stop-Function -Message "Compression failed for $instance - $db" -Target $db -ErrorRecord $_ -Continue + } + } + } + } +} +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Set-DbaDbQueryStoreOptions { + <# + .SYNOPSIS + Configure Query Store settings for a specific or multiple databases. + + .DESCRIPTION + Configure Query Store settings for a specific or multiple databases. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllDatabases + Run command against all user databases + + .PARAMETER State + Set the state of the Query Store. Valid options are "ReadWrite", "ReadOnly" and "Off". + + .PARAMETER FlushInterval + Set the flush to disk interval of the Query Store in seconds. + + .PARAMETER CollectionInterval + Set the runtime statistics collection interval of the Query Store in minutes. + + .PARAMETER MaxSize + Set the maximum size of the Query Store in MB. + + .PARAMETER CaptureMode + Set the query capture mode of the Query Store. Valid options are "Auto" and "All". + + .PARAMETER CleanupMode + Set the query cleanup mode policy. Valid options are "Auto" and "Off". + + .PARAMETER StaleQueryThreshold + Set the stale query threshold in days. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Changing Desired State" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: QueryStore + Author: Enrico van de Laar ( @evdlaar ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaQueryStoreOptions + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode All -CleanupMode Auto -StaleQueryThreshold 100 -AllDatabases + + Configure the Query Store settings for all user databases in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -FlushInterval 600 + + Only configure the FlushInterval setting for all Query Store databases in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -Database AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 + + Configure the Query Store settings for the AdventureWorks database in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -Exclude AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 + + Configure the Query Store settings for all user databases except the AdventureWorks database in the ServerA\SQL Instance. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [ValidateSet('ReadWrite', 'ReadOnly', 'Off')] + [string[]]$State, + [int64]$FlushInterval, + [int64]$CollectionInterval, + [int64]$MaxSize, + [ValidateSet('Auto', 'All')] + [string[]]$CaptureMode, + [ValidateSet('Auto', 'Off')] + [string[]]$CleanupMode, + [int64]$StaleQueryThreshold, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $ExcludeDatabase += 'master', 'tempdb' + } + + process { + if (!$Database -and !$ExcludeDatabase -and !$AllDatabases) { + Stop-Function -Message "You must specify a database(s) to execute against using either -Database, -ExcludeDatabase or -AllDatabases" + return + } + + if (!$State -and !$FlushInterval -and !$CollectionInterval -and !$MaxSize -and !$CaptureMode -and !$CleanupMode -and !$StaleQueryThreshold) { + Stop-Function -Message "You must specify something to change." + return + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 13 + + } + catch { + Stop-Function -Message "Can't connect to $instance. Moving on." -Category InvalidOperation -InnerErrorRecord $_ -Target $instance -Continue + } + + # We have to exclude all the system databases since they cannot have the Query Store feature enabled + $dbs = Get-DbaDatabase -SqlInstance $server -ExcludeDatabase $ExcludeDatabase -Database $Database | Where-Object IsAccessible + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $($db.name) on $instance" + + if ($db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "The database $db on server $instance is not accessible. Skipping database." + continue + } + + if ($State) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DesiredState to $state")) { + $db.QueryStoreOptions.DesiredState = $State + $db.QueryStoreOptions.Alter() + $db.QueryStoreOptions.Refresh() + } + } + + if ($db.QueryStoreOptions.DesiredState -eq "Off" -and (Test-Bound -Parameter State -Not)) { + Write-Message -Level Warning -Message "State is set to Off; cannot change values. Please update State to ReadOnly or ReadWrite." + continue + } + + if ($FlushInterval) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DataFlushIntervalInSeconds to $FlushInterval")) { + $db.QueryStoreOptions.DataFlushIntervalInSeconds = $FlushInterval + } + } + + if ($CollectionInterval) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StatisticsCollectionIntervalInMinutes to $CollectionInterval")) { + $db.QueryStoreOptions.StatisticsCollectionIntervalInMinutes = $CollectionInterval + } + } + + if ($MaxSize) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing MaxStorageSizeInMB to $MaxSize")) { + $db.QueryStoreOptions.MaxStorageSizeInMB = $MaxSize + } + } + + if ($CaptureMode) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing QueryCaptureMode to $CaptureMode")) { + $db.QueryStoreOptions.QueryCaptureMode = $CaptureMode + } + } + + if ($CleanupMode) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing SizeBasedCleanupMode to $CleanupMode")) { + $db.QueryStoreOptions.SizeBasedCleanupMode = $CleanupMode + } + } + + if ($StaleQueryThreshold) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StaleQueryThresholdInDays to $StaleQueryThreshold")) { + $db.QueryStoreOptions.StaleQueryThresholdInDays = $StaleQueryThreshold + } + } + + # Alter the Query Store Configuration + if ($Pscmdlet.ShouldProcess("$db on $instance", "Altering Query Store configuration on database")) { + try { + $db.QueryStoreOptions.Alter() + $db.Alter() + $db.Refresh() + } + catch { + Stop-Function -Message "Could not modify configuration." -Category InvalidOperation -InnerErrorRecord $_ -Target $db -Continue + } + } + + if ($Pscmdlet.ShouldProcess("$db on $instance", "Getting results from Get-DbaDbQueryStoreOptions")) { + # Display resulting changes + Get-DbaDbQueryStoreOptions -SqlInstance $server -Database $db.name -Verbose:$false + } + } + } + } +} + +function Set-DbaDbRecoveryModel { + <# + .SYNOPSIS + Set-DbaDbRecoveryModel sets the Recovery Model. + + .DESCRIPTION + Set-DbaDbRecoveryModel sets the Recovery Model for user databases. + + .PARAMETER SqlInstance + The target SQL Server instance or instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. if unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllDatabases + This is a parameter that was included for safety, so you don't accidentally set options on all databases without specifying + + .PARAMETER RecoveryModel + Recovery Model to be set. Valid options are 'Simple', 'Full', 'BulkLogged' + + Details about the recovery models can be found here: + https://docs.microsoft.com/en-us/sql/relational-databases/backup-restore/recovery-models-sql-server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + Prompts for confirmation. For example: + + Are you sure you want to perform this action? + Performing the operation "ALTER DATABASE [model] SET RECOVERY Full" on target "[model] on WERES14224". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .NOTES + Tags: Recovery, RecoveryModel, Simple, Full, Bulk, BulkLogged + Author: Viorel Ciucu (@viorelciucu), https://www.cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDbRecoveryModel + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel BulkLogged -Database model -Confirm:$true -Verbose + + Sets the Recovery Model to BulkLogged for database [model] on SQL Server instance sql2014. User is requested to confirm the action. + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2014 -Database TestDB | Set-DbaDbRecoveryModel -RecoveryModel Simple -Confirm:$false + + Sets the Recovery Model to Simple for database [TestDB] on SQL Server instance sql2014. Confirmation is not required. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel Simple -Database TestDB -Confirm:$false + + Sets the Recovery Model to Simple for database [TestDB] on SQL Server instance sql2014. Confirmation is not required. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel Simple -AllDatabases -Confirm:$false + + Sets the Recovery Model to Simple for ALL uses databases MODEL database on SQL Server instance sql2014. Runs without asking for confirmation. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel BulkLogged -Database TestDB1, TestDB2 -Confirm:$false -Verbose + + Sets the Recovery Model to BulkLogged for [TestDB1] and [TestDB2] databases on SQL Server instance sql2014. Runs without asking for confirmation. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess, ConfirmImpact = 'High')] + param ( + [parameter(Mandatory, ParameterSetName = "Instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [string]$RecoveryModel, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [switch]$EnableException, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Pipeline")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (!$Database -and !$AllDatabases -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify -AllDatabases or -Database to continue" + return + } + + # We need to be able to change the RecoveryModel for model database + $systemdbs = @("tempdb") + $databases = $server.Databases | Where-Object { $systemdbs -notcontains $_.Name -and $_.IsAccessible } + + # filter collection based on -Database/-Exclude parameters + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + if (!$databases) { + Stop-Function -Message "The database(s) you specified do not exist on the instance $instance." + return + } + + $InputObject += $databases + } + + foreach ($db in $InputObject) { + if ($db.RecoveryModel -eq $RecoveryModel) { + Stop-Function -Message "Recovery Model for database $db is already set to $RecoveryModel" -Category ConnectionError -Target $instance -Continue + } + else { + $db.RecoveryModel = $RecoveryModel; + if ($Pscmdlet.ShouldProcess("$db on $instance", "ALTER DATABASE $db SET RECOVERY $RecoveryModel")) { + $db.Alter() + Write-Message -Level Verbose -Message "Recovery Model set to $RecoveryModel for database $db" + } + } + Get-DbaDbRecoveryModel -SqlInstance $db.Parent -Database $db.name + } + } +} +function Set-DbaErrorLog { + <# + .SYNOPSIS + Set the configuration for the ErrorLog on a given SQL Server instance + .DESCRIPTION + Set the configuration for the ErrorLog on a given SQL Server instance. + Includes setting the number of log files configured and/or size in KB (SQL Server 2012+ only) + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER NumberOfLog + Integer value between 6 and 99 for setting the number of error log files to keep for SQL Server instance. + + .PARAMETER SizeInKb + Integer value for the size in KB that you want the error log file to grow. This is feature only in SQL Server 2012 and higher. When the file reaches that limit SQL Server will roll the error log over. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, ErrorLog + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: GNU GPL v3 https://opensource.org/licenses/GPL-3.0 + + .LINK + https://dbatools.io/Set-DbaErrorLog + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2017,server2014 -NumberOfLog 25 + + Sets the number of error log files to 25 on server2017 and server2014 + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2014 -SizeInKb 1024 + + Sets the size of the error log file, before it rolls over, to 1024KB (1GB) on server2014 + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2012 -NumberOfLog 25 -SizeInKb 500 + + Sets the number of error log files to 25 and size before it will roll over to 500KB on server2012 + #> + [cmdletbinding(SupportsShouldProcess)] + param( + [Parameter(ValueFromPipelineByPropertyName)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [DbaValidatePattern('^[6-9][0-9]?$', ErrorMessage = "Error processing {0} - input must be an integer between 6 and 99")] + [int]$NumberOfLog, + [int]$SizeInKb, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $collectionNumberLog = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Setting = 'NumberOfLogFiles' + OriginalNumberErrorLogs = $null + CurrentNumberErrorLogs = $null + Status = $null + } + $collectionErrorLogSize = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Setting = 'ErrorLogSizeKb' + OriginalErrorLogSizeKb = $null + CurrentErrorLogSizeKb = $null + Status = $null + } + if (Test-Bound 'NumberOfLog') { + try { + $currentNumLogs = $server.NumberOfLogFiles + $collectionNumberLog.OriginalNumberErrorLogs = $currentNumLogs + } + catch { + $collectionNumberLog.Status = "Failed collection" + $collectionNumberLog + Stop-Function -Message "Issue collecting current value for number of error logs" -Target $server -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + + if ($NumberOfLog -eq $currentNumLogs) { + Write-Message -Level Warning -Message "The provided value for NumberOfLog is already set" -Continue + } + else { + if ($PSCmdlet.ShouldProcess($server, "Setting number of logs from [$currentNumLogs] to [$NumberOfLog]")) { + try { + $server.NumberOfLogFiles = $NumberOfLog + $server.Alter() + } + catch { + $collectionNumberLog.Status = "Failed update" + $collectionNumberLog + Stop-Function -Message "Issue setting number of log files" -Target $instance -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + if ($PSCmdlet.ShouldProcess($server, "Output final results of setting number of log files")) { + $server.Refresh() + $collectionNumberLog.CurrentNumberErrorLogs = $server.NumberOfLogFiles + $collectionNumberLog + } + } + } + if (Test-Bound 'SizeInKb') { + try { + $currentSizeInKb = $server.ErrorInSizeKb + $collectionErrorLogSize.OriginalErrorLogSizeKb = $currentSizeInKb + } + catch { + $collectionErrorLogSize.Status = "Failed collection" + $collectionErrorLogSize + Stop-Function -Message "Issue collecting current value for number of error logs" -Target $server -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + + if ($SizeInKb -eq $currentSizeInKb) { + Write-Message -Level Warning -Message "The provided value for SizeInKb is already set" -Continue + } + else { + if ($PSCmdlet.ShouldProcess($server, "Setting number of logs from [$currentSizeInKb] to [$SizeInKb]")) { + try { + $server.ErrorLogSizeKb = $SizeInKb + $server.Alter() + } + catch { + $collectionErrorLogSize.Status = "Failed update" + $collectionErrorLogSize + Stop-Function -Message "Issue setting number of log files" -Target $instance -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + if ($PSCmdlet.ShouldProcess($server, "Output final results of setting error log size")) { + $server.Refresh() + $collectionErrorLogSize.CurrentErrorLogSizeKb = $server.ErrorLogSizeKb + $collectionErrorLogSize + } + } + } + } + } +} +function Set-DbaJobOwner { + <# + .SYNOPSIS + Sets SQL Agent job owners with a desired login if jobs do not match that owner. + + .DESCRIPTION + This function alters SQL Agent Job ownership to match a specified login if their current owner does not match the target login. By default, the target login will be 'sa', but the the user may specify a different login for ownership. This be applied to all jobs or only to a select collection of jobs. + + Best practice reference: http://sqlmag.com/blog/sql-server-tip-assign-ownership-jobs-sysadmin-account + + .NOTES + Tags: Agent, Job + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + Specifies the job(s) to process. Options for this list are auto-populated from the server. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + Specifies the job(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER Login + Specifies the login that you wish check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. This must be a valid security principal which exists on the target server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .LINK + https://dbatools.io/Set-DbaJobOwner + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa. + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost -Login DOMAIN\account + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match 'DOMAIN\account'. Note + that Login must be a valid security principal that exists on the target server. + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost -Job job1, job2 + + Sets SQL Agent Job owner to 'sa' on the job1 and job2 jobs if their current owner does not match 'sa'. + + .EXAMPLE + 'sqlserver','sql2016' | Set-DbaJobOwner + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa on both sqlserver and sql2016. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Jobs")] + [object[]]$Job, + [object[]]$ExcludeJob, + [Alias("TargetLogin")] + [string]$Login, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($servername in $SqlInstance) { + #connect to the instance + Write-Message -Level Verbose -Message "Connecting to $servername." + $server = Connect-SqlInstance $servername -SqlCredential $SqlCredential + + # dynamic sa name for orgs who have changed their sa name + if (!$Login) { + $Login = ($server.logins | Where-Object { $_.id -eq 1 }).Name + } + + #Validate login + if (($server.Logins.Name) -notcontains $Login) { + if ($SqlInstance.count -eq 1) { + throw -Message "Invalid login: $Login." + } + else { + Write-Message -Level Warning -Message "$Login is not a valid login on $servername. Moving on." + Continue + } + } + + if ($server.logins[$Login].LoginType -eq 'WindowsGroup') { + throw "$Login is a Windows Group and can not be a job owner." + } + + #Get database list. If value for -Job is passed, massage to make it a string array. + #Otherwise, use all jobs on the instance where owner not equal to -TargetLogin + Write-Message -Level Verbose -Message "Gathering jobs to update." + + if ($Job) { + $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $Login -and $Job -contains $_.Name } + } + else { + $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $Login } + } + + if ($ExcludeJob) { + $jobcollection = $jobcollection | Where-Object { $ExcludeJob -notcontains $_.Name } + } + + Write-Message -Level Verbose -Message "Updating $($jobcollection.Count) job(s)." + foreach ($j in $jobcollection) { + $jobname = $j.name + + if ($PSCmdlet.ShouldProcess($servername, "Setting job owner for $jobname to $Login")) { + try { + Write-Message -Level Verbose -Message "Setting job owner for $jobname to $Login on $servername." + #Set job owner to $TargetLogin (default 'sa') + $j.OwnerLoginName = $Login + $j.Alter() + } + catch { + Stop-Function -Message "Issue setting job owner on $jobName." -Target $jobName -InnerErrorRecord $_ -Category InvalidOperation + } + } + } + } + } +} +function Set-DbaLogin { + + <# + .SYNOPSIS + Set-DbaLogin makes it possible to make changes to one or more logins. + + .DESCRIPTION + Set-DbaLogin will enable you to change the password, unlock, rename, disable or enable, deny or grant login privileges to the login. + It's also possible to add or remove server roles from the login. + + .PARAMETER SqlInstance + SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Login + The login that needs to be changed + + .PARAMETER Password + The new password for the login This can be either a credential or a secure string. + + .PARAMETER Unlock + Switch to unlock an account. This will only be used in conjunction with the -Password parameter. + The default is false. + + .PARAMETER MustChange + Does the user need to change his/her password. This will only be used in conjunction with the -Password parameter. + The default is false. + + .PARAMETER NewName + The new name for the login. + + .PARAMETER Disable + Disable the login + + .PARAMETER Enable + Enable the login + + .PARAMETER DenyLogin + Deny access to SQL Server + + .PARAMETER GrantLogin + Grant access to SQL Server + + .PARAMETER PasswordPolicyEnforced + Should the password policy be enforced. + + .PARAMETER AddRole + Add one or more server roles to the login + The following roles can be used "bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin". + + .PARAMETER RemoveRole + Remove one or more server roles to the login + The following roles can be used "bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin". + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Original Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Login + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaLogin + + .EXAMPLE + $password = ConvertTo-SecureString "PlainTextPassword" -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential ("username", $password) + Set-DbaLogin -SqlInstance sql1 -Login login1 -Password $cred -Unlock -MustChange + + Set the new password for login1 using a credential, unlock the account and set the option + that the usermust change password at next logon. + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -Enable + + Enable the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1, login2, login3, login4 -Enable + + Enable multiple logins + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1, sql2, sql3 -Login login1, login2, login3, login4 -Enable + + Enable multiple logins on multiple instances + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -Disable + + Disable the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -DenyLogin + + Deny the login to connect to the instance + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -GrantLogin + + Grant the login to connect to the instance + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -PasswordPolicyEnforced + + Enforces the password policy on a login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -PasswordPolicyEnforced:$false + + Disables enforcement of the password policy on a login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login test -AddRole serveradmin + + Add the server role "serveradmin" to the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login test -RemoveRole bulkadmin + + Remove the server role "bulkadmin" to the login + +#> + + [CmdletBinding()] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [string[]]$Login, + [object]$Password, + [switch]$Unlock, + [switch]$MustChange, + [string]$NewName, + [switch]$Disable, + [switch]$Enable, + [switch]$DenyLogin, + [switch]$GrantLogin, + [switch]$PasswordPolicyEnforced, + [ValidateSet("bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin")] + [string[]]$AddRole, + [ValidateSet("bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin")] + [string[]]$RemoveRole, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + # Check the parameters + if ($Login -eq $NewName) { + Stop-Function -Message "Login name is the same as the value in -NewName" -Target $Login -Continue + } + + if ($Disable -and $Enable) { + Stop-Function -Message "You cannot use both -Enable and -Disable together" -Target $Login -Continue + } + + if ($GrantLogin -and $DenyLogin) { + Stop-Function -Message "You cannot use both -GrantLogin and -DenyLogin together" -Target $Login -Continue + } + + # Check the password + if ($Password) { + switch ($Password.GetType().Name) { + "PSCredential" { $newPassword = $Password.Password} + "SecureString" { $newPassword = $Password} + } + } + else { + } + + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Get all the logins + $allLogins = $server.Logins | Where-Object {($_.IsSystemObject -eq $false) -and ($_.Name -notlike '##*')} + $logins = $server.Logins | Where-Object {$Login -contains $_.Name} + + # Loop through all the logins + foreach ($l in $logins) { + + # Create the notes + $notes = @() + + # Change the name + if ($NewName) { + # Check if the new name doesn't already exist + if ($allLogins.Name -notcontains $NewName) { + try { + $l.Rename($NewName) + } + catch { + $notes += "Couldn't rename login" + Stop-Function -Message "Something went wrong changing the name for $l" -Target $l -ErrorRecord $_ -Continue + } + } + else { + $notes += "New login name already exists" + Write-Message -Message "New login name $NewName already exists on $instance" -Level Verbose + } + } + + # Change the password + if ($Password) { + try { + $l.ChangePassword($newPassword, $Unlock, $MustChange) + $passwordChanged = $true + } + catch { + $notes += "Couldn't change password" + $passwordChanged = $false + Stop-Function -Message "Something went wrong changing the password for $l" -Target $l -ErrorRecord $_ -Continue + } + } + + # Disable the login + if ($Disable) { + if ($l.IsDisabled) { + Write-Message -Message "Login $l is already disabled" -Level Verbose + } + else { + try { + $l.Disable() + } + catch { + $notes += "Couldn't disable login" + Stop-Function -Message "Something went wrong disabling $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Enable the login + if ($Enable) { + if (-not $l.IsDisabled) { + Write-Message -Message "Login $l is already enabled" -Level Verbose + } + else { + try { + $l.Enable() + } + catch { + $notes += "Couldn't enable login" + Stop-Function -Message "Something went wrong enabling $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Deny access + if ($DenyLogin) { + if ($l.DenyWindowsLogin) { + Write-Message -Message "Login $l already has login access denied" -Level Verbose + } + else { + $l.DenyWindowsLogin = $true + } + } + + # Grant access + if ($GrantLogin) { + if (-not $l.DenyWindowsLogin) { + Write-Message -Message "Login $l already has login access granted" -Level Verbose + } + else { + $l.DenyWindowsLogin = $false + } + } + + # Enforce password policy + if (Test-Bound PasswordPolicyEnforced) { + if ($l.PasswordPolicyEnforced -eq $PasswordPolicyEnforced) { + Write-Message -Message ("Login $l password policy is already set to " + $l.PasswordPolicyEnforced) -Level Verbose + } else { + $l.PasswordPolicyEnforced = $PasswordPolicyEnforced + } + } + + # Add server roles to login + if ($AddRole) { + # Loop through each of the roles + foreach ($role in $AddRole) { + try { + $l.AddToRole($role) + } + catch { + $notes += "Couldn't add role $role" + Stop-Function -Message "Something went wrong adding role $role to $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Remove server roles from login + if ($RemoveRole) { + # Loop through each of the roles + foreach ($role in $RemoveRole) { + try { + $server.Roles[$role].DropMember($l.Name) + } + catch { + $notes += "Couldn't remove role $role" + Stop-Function -Message "Something went wrong removing role $role to $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Alter the login to make the changes + $l.Alter() + + # Retrieve the server roles for the login + $roles = Get-DbaRoleMember -SqlInstance $instance -IncludeServerLevel | Where-Object {$null -eq $_.Database -and $_.Member -eq $l.Name} + + # Check if there were any notes to include in the results + if ($notes) { + $notes = $notes | Get-Unique + $notes = $notes -Join ';' + } + else { + $notes = $null + } + + # Return the results + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LoginName = $l.Name + DenyLogin = $l.DenyWindowsLogin + IsDisabled = $l.IsDisabled + IsLocked = $l.IsLocked + PasswordPolicyEnforced = $l.PasswordPolicyEnforced + MustChangePassword = $l.MustChangePassword + PasswordChanged = $passwordChanged + ServerRole = $roles.Role -join "," + Notes = $notes + } | Select-DefaultView -ExcludeProperty Login + + } # end for each login + + } # end for each instance + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing login(s)" -Level Verbose + } + + +} +function Set-DbaMaxDop { + <# + .SYNOPSIS + Sets SQL Server maximum degree of parallelism (Max DOP), then displays information relating to SQL Server Max DOP configuration settings. Works on SQL Server 2005 and higher. + + .DESCRIPTION + Uses the Test-DbaMaxDop command to get the recommended value if -MaxDop parameter is not specified. + + These are just general recommendations for SQL Server and are a good starting point for setting the "max degree of parallelism" option. + + You can set MaxDop database scoped configurations if the server is version 2016 or higher + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more databases to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more databases to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER MaxDop + Specifies the Max DOP value to set. + + .PARAMETER AllDatabases + If this switch is enabled, Max DOP will be set on all databases. This switch is only useful on SQL Server 2016 and higher. + + .PARAMETER Collection + If Test-SQLMaxDop has been executed prior to this function, the results may be passed in via this parameter. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .NOTES + Tags: MaxDop, SpConfigure + Author: Claudio Silva (@claudioessilva) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaMaxDop + + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2008, sql2012 + + Sets Max DOP to the recommended value for servers sql2008 and sql2012. + + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2014 -MaxDop 4 + + Sets Max DOP to 4 for server sql2014. + + .EXAMPLE + Test-DbaMaxDop -SqlInstance sql2008 | Set-DbaMaxDop + + Gets the recommended Max DOP from Test-DbaMaxDop and applies it to to sql2008. + + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2016 -Database db1 + + Set recommended Max DOP for database db1 on server sql2016. + + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2016 -AllDatabases + + Set recommended Max DOP for all databases on server sql2016. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [int]$MaxDop = -1, + [Parameter(ValueFromPipeline = $True)] + [object]$Collection, + [Alias("All")] + [switch]$AllDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $processed = New-Object System.Collections.ArrayList + $results = @() + } + process { + if ((Test-Bound -Parameter Database) -and (Test-Bound -Parameter AllDatabases) -and (Test-Bound -Parameter ExcludeDatabase)) { + Stop-Function -Category InvalidArgument -Message "-Database, -AllDatabases and -ExcludeDatabase are mutually exclusive. Please choose only one. Quitting." + return + } + + $dbscopedconfiguration = $false + + if ($MaxDop -eq -1) { + $UseRecommended = $true + } + + if ((Test-Bound -Not -Parameter Collection)) { + $collection = Test-DbaMaxDop -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Verbose:$false + } + elseif ($null -eq $collection.SqlInstance) { + $collection = Test-DbaMaxDop -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Verbose:$false + } + + $collection | Add-Member -Force -NotePropertyName OldInstanceMaxDopValue -NotePropertyValue 0 + $collection | Add-Member -Force -NotePropertyName OldDatabaseMaxDopValue -NotePropertyValue 0 + + #If we have servers 2016 or higher we will have a row per database plus the instance level, getting unique we only run one time per instance + $servers = $collection | Select-Object SqlInstance -Unique + + foreach ($server in $servers) { + $servername = $server.SqlInstance + + Write-Message -Level Verbose -Message "Connecting to $servername" + try { + $server = Connect-SqlInstance -SqlInstance $servername -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $servername -Continue + } + + if (!(Test-SqlSa -SqlInstance $server)) { + Stop-Function -Message "Not a sysadmin on $server. Skipping." -Category PermissionDenied -ErrorRecord $_ -Target $currentServer -Continue + } + + if ($server.versionMajor -ge 13) { + Write-Message -Level Verbose -Message "Server '$servername' supports Max DOP configuration per database." + + if ((Test-Bound -Not -Parameter Database) -and (Test-Bound -Not -Parameter ExcludeDatabase)) { + #Set at instance level + $collection = $collection | Where-Object { $_.DatabaseMaxDop -eq "N/A" } + } + else { + $dbscopedconfiguration = $true + + if ((Test-Bound -Not -Parameter AllDatabases) -and (Test-Bound -Parameter Database)) { + $collection = $collection | Where-Object { $_.Database -in $Database } + } + elseif ((Test-Bound -Not -Parameter AllDatabases) -and (Test-Bound -Parameter ExcludeDatabase)) { + $collection = $collection | Where-Object { $_.Database -notin $ExcludeDatabase } + } + else { + if (Test-Bound -Parameter AllDatabases) { + $collection = $collection | Where-Object { $_.DatabaseMaxDop -ne "N/A" } + } + else { + $collection = $collection | Where-Object { $_.DatabaseMaxDop -eq "N/A" } + $dbscopedconfiguration = $false + } + } + } + } + else { + if ((Test-Bound -Parameter database) -or (Test-Bound -Parameter AllDatabases)) { + Write-Message -Level Warning -Message "Server '$servername' (v$($server.versionMajor)) does not support Max DOP configuration at the database level. Remember that this option is only available from SQL Server 2016 (v13). Run the command again without using database related parameters. Skipping." + Continue + } + } + + foreach ($row in $collection | Where-Object { $_.SqlInstance -eq $servername }) { + if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.CurrentInstanceMaxDop) -and !($dbscopedconfiguration)) { + Write-Message -Level Verbose -Message "$servername is configured properly. No change required." + Continue + } + + if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.DatabaseMaxDop) -and $dbscopedconfiguration) { + Write-Message -Level Verbose -Message "Database $($row.Database) on $servername is configured properly. No change required." + Continue + } + + $row.OldInstanceMaxDopValue = $row.CurrentInstanceMaxDop + + try { + if ($UseRecommended) { + if ($dbscopedconfiguration) { + $row.OldDatabaseMaxDopValue = $row.DatabaseMaxDop + + if ($resetDatabases) { + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP to $($row.DatabaseMaxDop)." + $server.Databases["$($row.Database)"].MaxDop = $row.DatabaseMaxDop + } + else { + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $($row.RecommendedMaxDop)." + $server.Databases["$($row.Database)"].MaxDop = $row.RecommendedMaxDop + $row.DatabaseMaxDop = $row.RecommendedMaxDop + } + + } + else { + Write-Message -Level Verbose -Message "Changing $server SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $($row.RecommendedMaxDop)." + $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $row.RecommendedMaxDop + $row.CurrentInstanceMaxDop = $row.RecommendedMaxDop + } + } + else { + if ($dbscopedconfiguration) { + $row.OldDatabaseMaxDopValue = $row.DatabaseMaxDop + + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $MaxDop." + $server.Databases["$($row.Database)"].MaxDop = $MaxDop + $row.DatabaseMaxDop = $MaxDop + } + else { + Write-Message -Level Verbose -Message "Changing $servername SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $MaxDop." + $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $MaxDop + $row.CurrentInstanceMaxDop = $MaxDop + } + } + + if ($dbscopedconfiguration) { + if ($Pscmdlet.ShouldProcess($row.Database, "Setting max dop on database")) { + $server.Databases["$($row.Database)"].Alter() + } + } + else { + if ($Pscmdlet.ShouldProcess($servername, "Setting max dop on instance")) { + $server.Configuration.Alter() + } + } + + $results += [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + InstanceVersion = $row.InstanceVersion + Database = $row.Database + DatabaseMaxDop = $row.DatabaseMaxDop + CurrentInstanceMaxDop = $row.CurrentInstanceMaxDop + RecommendedMaxDop = $row.RecommendedMaxDop + OldDatabaseMaxDopValue = $row.OldDatabaseMaxDopValue + OldInstanceMaxDopValue = $row.OldInstanceMaxDopValue + } + } + catch { + Stop-Function -Message "Could not modify Max Degree of Parallelism for $server." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($dbscopedconfiguration) { + Select-DefaultView -InputObject $results -Property InstanceName, Database, OldDatabaseMaxDopValue, @{ name = "CurrentDatabaseMaxDopValue"; expression = { $_.DatabaseMaxDop } } + } + else { + Select-DefaultView -InputObject $results -Property InstanceName, OldInstanceMaxDopValue, CurrentInstanceMaxDop + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Set-DbaMaxMemory { + <# + .SYNOPSIS + Sets SQL Server 'Max Server Memory' configuration setting to a new value then displays information this setting. + + .DESCRIPTION + Sets SQL Server max memory then displays information relating to SQL Server Max Memory configuration settings. + + Inspired by Jonathan Kehayias's post about SQL Server Max memory (http://bit.ly/sqlmemcalc), this uses a formula to + determine the default optimum RAM to use, then sets the SQL max value to that number. + + Jonathan notes that the formula used provides a *general recommendation* that doesn't account for everything that may + be going on in your specific environment. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER MaxMB + Specifies the max megabytes + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .NOTES + Tags: MaxMemory, Memory + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaMaxMemory + + .EXAMPLE + Set-DbaMaxMemory sqlserver1 + + Set max memory to the recommended MB on just one server named "sqlserver1" + + .EXAMPLE + Set-DbaMaxMemory -SqlInstance sqlserver1 -MaxMB 2048 + + Explicitly max memory to 2048 MB on just one server, "sqlserver1" + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver | Test-DbaMaxMemory | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } | Set-DbaMaxMemory + + Find all servers in SQL Server Central Management server that have Max SQL memory set to higher than the total memory + of the server (think 2147483647), then pipe those to Set-DbaMaxMemory and use the default recommendation. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param ( + [Parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer", "SqlServers", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Parameter(Position = 1)] + [int]$MaxMB, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ((Test-Bound -Not -Parameter SqlInstance) -and (Test-Bound -Not -Parameter Collection)) { + Stop-Function -Category InvalidArgument -Message "You must specify a server list source using -SqlInstance or you can pipe results from Test-DbaMaxMemory" + return + } + + if ($MaxMB -eq 0) { + $UseRecommended = $true + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (!(Test-SqlSa -SqlInstance $server)) { + Stop-Function -Message "Not a sysadmin on $server. Skipping." -Category PermissionDenied -ErrorRecord $_ -Target $server -Continue + } + + try { + $currentServer = Test-DbaMaxMemory -SqlInstance $server + Add-Member -Force -InputObject $currentServer -NotePropertyName OldMaxValue -NotePropertyValue 0 + $currentServer.OldMaxValue = $currentServer.SqlMaxMB + } + catch { + Stop-Function -Message "Issue collecting memory information on $server" -Target $server -ErrorRecord $_ -InnerException $_.Exception -Continue + } + + try { + if ($UseRecommended) { + Write-Message -Level Verbose -Message "Change $server SQL Server Max Memory from $($currentServer.SqlMaxMB) to $($currentServer.RecommendedMB) MB" + + if ($currentServer.RecommendedMB -eq 0 -or $null -eq $currentServer.RecommendedMB) { + $maxMem = (Test-DbaMaxMemory -SqlInstance $server).RecommendedMB + Write-Message -Level VeryVerbose -Message "Max memory recommended: $maxMem" + $server.Configuration.MaxServerMemory.ConfigValue = $maxMem + } + else { + $server.Configuration.MaxServerMemory.ConfigValue = $currentServer.RecommendedMB + } + } + else { + Write-Message -Level Verbose -Message "Change $server SQL Server Max Memory from $($currentServer.SqlMaxMB) to $MaxMB MB" + $server.Configuration.MaxServerMemory.ConfigValue = $MaxMB + } + if ($PSCmdlet.ShouldProcess($server, "Change Max Memory from $($currentServer.OldMaxValue) to $($server.Configuration.MaxServerMemory.ConfigValue)")) { + try { + $server.Configuration.Alter() + $currentServer.SqlMaxMB = $server.Configuration.MaxServerMemory.ConfigValue + } + catch { + Stop-Function -Message "Failed to apply configuration change for $server" -ErrorRecord $_ -Target $server -Continue + } + } + } + catch { + Stop-Function -Message "Could not modify Max Server Memory for $server" -ErrorRecord $_ -Target $server -Continue + } + + Add-Member -InputObject $currentServer -Force -MemberType NoteProperty -Name CurrentMaxValue -Value $currentServer.SqlMaxMB + Select-DefaultView -InputObject $currentServer -Property ComputerName, InstanceName, SqlInstance, TotalMB, OldMaxValue, CurrentMaxValue + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Set-DbaNetworkCertificate { + <# + .SYNOPSIS + Sets the network certificate for SQL Server instance + + .DESCRIPTION + Sets the network certificate for SQL Server instance. This setting is found in Configuration Manager. + + This command also grants read permissions for the service account on the certificate's private key. + + References: + http://sqlmag.com/sql-server/7-steps-ssl-encryption + https://azurebi.jppp.org/2016/01/23/using-lets-encrypt-certificates-for-secure-sql-server-connections/ + https://blogs.msdn.microsoft.com/sqlserverfaq/2016/09/26/creating-and-registering-ssl-certificates/ + + .PARAMETER SqlInstance + The target SQL Server - defaults to localhost. + + .PARAMETER Credential + Allows you to login to the computer (not sql instance) using alternative credentials. + + .PARAMETER Certificate + The target certificate object + + .PARAMETER Thumbprint + The thumbprint of the target certificate + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + New-DbaComputerCertificate | Set-DbaNetworkCertificate -SqlInstance localhost\SQL2008R2SP2 + + Creates and imports a new certificate signed by an Active Directory CA on localhost then sets the network certificate for the SQL2008R2SP2 to that newly created certificate. + + .EXAMPLE + Set-DbaNetworkCertificate -SqlInstance sql1\SQL2008R2SP2 -Thumbprint 1223FB1ACBCA44D3EE9640F81B6BA14A92F3D6E2 + + Sets the network certificate for the SQL2008R2SP2 instance to the certificate with the thumbprint of 1223FB1ACBCA44D3EE9640F81B6BA14A92F3D6E2 in LocalMachine\My on sql1 + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low", DefaultParameterSetName = 'Default')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [PSCredential] + + $Credential, + + [parameter(Mandatory, ParameterSetName = "Certificate", ValueFromPipeline)] + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, + + [parameter(Mandatory, ParameterSetName = "Thumbprint")] + [string] + $Thumbprint, + + [switch] + [Alias('Silent')]$EnableException + ) + + process { + if (Test-FunctionInterrupt) { return } + $Certificate + if (!$Certificate -and !$Thumbprint) { + Stop-Function -Message "You must specify a certificate or thumbprint" + return + } + + if (!$Thumbprint) { + Write-Message -Level SomewhatVerbose -Message "Getting thumbprint" + $Thumbprint = $Certificate.Thumbprint + } + + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname" + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance" -Target $instance -Continue -Category InvalidArgument + } + + $computername = $instance.ComputerName + $instancename = $instance.instancename + Write-Message -Level Output -Message "Connecting to SQL WMI on $computername" + + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instancename)" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + if (-not $sqlwmi) { + Stop-Function -Message "Cannot find $instancename on $computerName" -Continue -Category ObjectNotFound -Target $instance + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance" -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + $Thumbprint = $args[4] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$regroot\MSSQLServer\SuperSocketNetLib" + + $oldthumbprint = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + + $cert = Get-ChildItem Cert:\LocalMachine -Recurse -ErrorAction Stop | Where-Object { $_.Thumbprint -eq $Thumbprint } + + if ($null -eq $cert) { + Write-Warning "Certificate does not exist on $env:COMPUTERNAME" + return + } + + $permission = $serviceaccount, "Read", "Allow" + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyFullPath = $keyPath + $keyName + + $acl = Get-Acl -Path $keyFullPath + $null = $acl.AddAccessRule($accessRule) + Set-Acl -Path $keyFullPath -AclObject $acl + + if ($acl) { + Set-ItemProperty -Path $regpath -Name Certificate -Value $Thumbprint.ToString().ToLower() # to make it compat with SQL config + } + else { + Write-Warning "Read-only permissions could not be granted to certificate" + return + } + + if (![System.String]::IsNullOrEmpty($oldthumbprint)) { + $notes = "Granted $serviceaccount read access to certificate private key. Replaced thumbprint: $oldthumbprint." + } + else { + $notes = "Granted $serviceaccount read access to certificate private key" + } + + $newthumbprint = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + CertificateThumbprint = $newthumbprint + Notes = $notes + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $instanceName to import new cert")) { + try { + Invoke-Command2 -Raw -ComputerName $resolved.fqdn -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname, $Thumbprint -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.fqdn) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} +function Set-DbaPowerPlan { + <# + .SYNOPSIS + Sets the SQL Server OS's Power Plan. + + .DESCRIPTION + Sets the SQL Server OS's Power Plan. Defaults to High Performance which is best practice. + + If your organization uses a custom power plan that is considered best practice, specify -CustomPowerPlan. + + References: + https://support.microsoft.com/en-us/kb/2207548 + http://www.sqlskills.com/blogs/glenn/windows-power-plan-effects-on-newer-intel-processors/ + + .PARAMETER ComputerName + The server(s) to set the Power Plan on. + + .PARAMETER PowerPlan + Specifies the Power Plan that you wish to use. Valid options for this match the Windows default Power Plans of "Power Saver", "Balanced", and "High Performance". + + .PARAMETER CustomPowerPlan + Specifies the name of a custom Power Plan to use. + + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: PowerPlan, OS, Configure + Requires: WMI access to servers + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaPowerPlan + + .EXAMPLE + Set-DbaPowerPlan -ComputerName sqlserver2014a + + Sets the Power Plan to High Performance. Skips it if its already set. + + .EXAMPLE + Set-DbaPowerPlan -ComputerName sqlcluster -CustomPowerPlan 'Maximum Performance' + + Sets the Power Plan to the custom power plan called "Maximum Performance". Skips it if its already set. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [object[]]$ComputerName, + [ValidateSet('High Performance', 'Balanced', 'Power saver')] + [string]$PowerPlan = 'High Performance', + [string]$CustomPowerPlan, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($CustomPowerPlan.Length -gt 0) { + $PowerPlan = $CustomPowerPlan + } + + function Set-DbaPowerPlanInternal { + param($server) + + try { + Write-Message -Level Verbose -Message "Testing connection to $server and resolving IP address." + $ipaddr = (Test-Connection $server -Count 1 -ErrorAction SilentlyContinue).Ipv4Address | Select-Object -First 1 + + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + try { + Write-Message -Level Verbose -Message "Getting Power Plan information from $server." + $query = "Select ElementName from Win32_PowerPlan WHERE IsActive = 'true'" + $currentplan = Get-WmiObject -Namespace Root\CIMV2\Power -ComputerName $ipaddr -Query $query -ErrorAction SilentlyContinue + $currentplan = $currentplan.ElementName + } + catch { + Stop-Function -Message "Can't connect to WMI on $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + if ($null -eq $currentplan) { + # the try/catch above isn't working, so make it silent and handle it here. + Stop-Function -Message "Cannot get Power Plan for $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + $planinfo = [PSCustomObject]@{ + Server = $server + PreviousPowerPlan = $currentplan + ActivePowerPlan = $PowerPlan + } + + if ($PowerPlan -ne $currentplan) { + if ($Pscmdlet.ShouldProcess($server, "Changing Power Plan from $CurrentPlan to $PowerPlan")) { + try { + Write-Message -Level Verbose -Message "Setting Power Plan to $PowerPlan." + $null = (Get-WmiObject -Name root\cimv2\power -ComputerName $ipaddr -Class Win32_PowerPlan -Filter "ElementName='$PowerPlan'").Activate() + } + catch { + Stop-Function -Message "Couldn't set Power Plan on $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + } + } + else { + if ($Pscmdlet.ShouldProcess($server, "Stating power plan is already set to $PowerPlan, won't change.")) { + Write-Message -Level Verbose -Message "PowerPlan on $server is already set to $PowerPlan. Skipping." + } + } + + return $planinfo + } + + + $collection = New-Object System.Collections.ArrayList + $processed = New-Object System.Collections.ArrayList + } + + process { + foreach ($server in $ComputerName) { + if ($server -match 'Server\=') { + Write-Message -Level Verbose -Message "Matched that value was piped from Test-DbaPowerPlan." + # I couldn't properly unwrap the output from Test-DbaPowerPlan so here goes. + $lol = $server.Split("\;")[0] + $lol = $lol.TrimEnd("\}") + $lol = $lol.TrimStart("\@\{Server") + # There was some kind of parsing bug here, don't clown + $server = $lol.TrimStart("\=") + } + + if ($server -match '\\') { + $server = $server.Split('\\')[0] + } + + if ($server -notin $processed) { + $null = $processed.Add($server) + Write-Message -Level Verbose -Message "Connecting to $server." + } + else { + continue + } + + $data = Set-DbaPowerPlanInternal $server + + if ($data.Count -gt 1) { + $data.GetEnumerator() | ForEach-Object { $null = $collection.Add($_) } + } + else { + $null = $collection.Add($data) + } + } + } + + end { + If ($Pscmdlet.ShouldProcess("console", "Showing results")) { + return $collection + } + } +} +function Set-DbaPrivilege { + <# + .SYNOPSIS + Adds the SQL Service account to local privileges on one or more computers. + + .DESCRIPTION + Adds the SQL Service account to local privileges 'Lock Pages in Memory', 'Instant File Initialization', 'Logon as Batch' on one or more computers. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Type + Use this to choose the privilege(s) to which you want to add the SQL Service account. + Accepts 'IFI', 'LPIM' and/or 'BatchLogon' for local privileges 'Instant File Initialization', 'Lock Pages in Memory' and 'Logon as Batch'. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Privilege + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaPrivilege + + .EXAMPLE + Set-DbaPrivilege -ComputerName sqlserver2014a -Type LPIM,IFI + + Adds the SQL Service account(s) on computer sqlserver2014a to the local privileges 'SeManageVolumePrivilege' and 'SeLockMemoryPrivilege'. + + .EXAMPLE + 'sql1','sql2','sql3' | Set-DbaPrivilege -Type IFI + + Adds the SQL Service account(s) on computers sql1, sql2 and sql3 to the local privilege 'SeManageVolumePrivilege'. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [dbainstanceparameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Parameter(Mandatory = $true)] + [ValidateSet('IFI', 'LPIM', 'BatchLogon')] + [string[]]$Type, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + $ResolveAccountToSID = @" +function Convert-UserNameToSID ([string] `$Acc ) { +`$objUser = New-Object System.Security.Principal.NTAccount(`"`$Acc`") +`$strSID = `$objUser.Translate([System.Security.Principal.SecurityIdentifier]) +`$strSID.Value +} +"@ + $ComputerName = $ComputerName.ComputerName | Select-Object -Unique + } + process { + foreach ($computer in $ComputerName) { + try { + Write-Message -Level Verbose -Message "Connecting to $computer" + $null = Test-ElevationRequirement -ComputerName $Computer -Continue + if (Test-PSRemoting -ComputerName $Computer) { + Write-Message -Level Verbose -Message "Exporting Privileges on $Computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); secedit /export /cfg $temp\secpolByDbatools.cfg > $NULL; + } + Write-Message -Level Verbose -Message "Getting SQL Service Accounts on $computer" + $SQLServiceAccounts = (Get-DbaSqlService -ComputerName $computer -Type Engine).StartName + if ($SQLServiceAccounts.count -ge 1) { + Write-Message -Level Verbose -Message "Setting Privileges on $Computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -Verbose -ArgumentList $ResolveAccountToSID, $SQLServiceAccounts, $BatchLogon, $IFI, $LPIM -ScriptBlock { + [CmdletBinding()] + Param ($ResolveAccountToSID, + $SQLServiceAccounts, + $BatchLogon, + $IFI, + $LPIM) + . ([ScriptBlock]::Create($ResolveAccountToSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + $tempfile = "$temp\secpolByDbatools.cfg" + if ('BatchLogon' -in $Type) { + $BLline = Get-Content $tempfile | Where-Object { $_ -match "SeBatchLogonRight" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($BLline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeBatchLogonRight = ", "SeBatchLogonRight = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Batch Logon Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Batch Logon Privilege on $env:ComputerName" + } + } + } + if ('IFI' -in $Type) { + $IFIline = Get-Content $tempfile | Where-Object { $_ -match "SeManageVolumePrivilege" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($IFIline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeManageVolumePrivilege = ", "SeManageVolumePrivilege = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Instant File Initialization Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Instant File Initialization Privilege on $env:ComputerName" + } + } + } + if ('LPIM' -in $Type) { + $LPIMline = Get-Content $tempfile | Where-Object { $_ -match "SeLockMemoryPrivilege" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($LPIMline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeLockMemoryPrivilege = ", "SeLockMemoryPrivilege = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Lock Pages in Memory Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Lock Pages in Memory Privilege on $env:ComputerName" + } + } + } + $null = secedit /configure /cfg $tempfile /db secedit.sdb /areas USER_RIGHTS /overwrite /quiet + } -ErrorAction SilentlyContinue + Write-Message -Level Verbose -Message "Removing secpol file on $computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); Remove-Item $temp\secpolByDbatools.cfg -Force > $NULL } + } + else { + Write-Message -Level Warning -Message "No SQL Service Accounts found on $Computer" + } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} +function Set-DbaSpConfigure { + <# + .SYNOPSIS + Changes the server level system configuration (sys.configuration/sp_configure) value for a given configuration + + .DESCRIPTION + This function changes the configured value for sp_configure settings. If the setting is dynamic this setting will be used, otherwise the user will be warned that a restart of SQL is required. + This is designed to be safe and will not allow for configurations to be set outside of the defined configuration min and max values. + While it is possible to set below the min, or above the max this can cause serious problems with SQL Server (including startup failures), and so is not permitted. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a + collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The name of the configuration to be set -- Configs is auto-populated for tabbing convenience. + + .PARAMETER Value + The new value for the configuration + + .PARAMETER InputObject + Piped objectgs from Get-DbaSpConfigure + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: SpConfigure + Author: Nic Cain, https://sirsql.net/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaSpConfigure + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name ScanForStartupProcedures -Value 1 + + Adjusts the Scan for startup stored procedures configuration value to 1 and notifies the user that this requires a SQL restart to take effect + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance sql2017, sql2014 -Name XPCmdShellEnabled, IsSqlClrEnabled | Set-DbaSpConfigure -Value $false + Sets the values for XPCmdShellEnabled and IsSqlClrEnabled on sql2017 and sql2014 to False + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name XPCmdShellEnabled -Value 1 + + Adjusts the xp_cmdshell configuration value to 1. + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name XPCmdShellEnabled -Value 1 -WhatIf + + Returns information on the action that would be performed. No actual change will be made. + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential]$SqlCredential, + [Alias("NewValue", "NewConfig")] + [int]$Value, + [Alias("Config", "ConfigName")] + [string[]]$Name, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + process { + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaSpConfigure -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Name $Name + } + + foreach ($configobject in $InputObject) { + $server = $InputObject.Parent + $currentRunValue = $configobject.RunningValue + $minValue = $configobject.MinValue + $maxValue = $configobject.MaxValue + $isDynamic = $configobject.IsDynamic + $configuration = $configobject.Name + + #Let us not waste energy setting the value to itself + if ($currentRunValue -eq $value) { + Stop-Function -Message "Value to set is the same as the existing value. No work being performed." -Continue -Target $server -Category InvalidData + } + + #Going outside the min/max boundary can be done, but it can break SQL, so I don't think allowing that is wise at this juncture + if ($value -lt $minValue -or $value -gt $maxValue) { + Stop-Function -Message "Value out of range for $configuration ($minValue <-> $maxValue)" -Continue -Category InvalidArgument + } + + If ($Pscmdlet.ShouldProcess($SqlInstance, "Adjusting server configuration $configuration from $currentRunValue to $value.")) { + try { + $server.Configuration.$configuration.ConfigValue = $value + $server.Configuration.Alter() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + ConfigName = $configuration + OldValue = $currentRunValue + NewValue = $value + } + + #If it's a dynamic setting we're all clear, otherwise let the user know that SQL needs to be restarted for the change to take + if ($isDynamic -eq $false) { + Write-Message -Level Warning -Message "Configuration setting $configuration has been set, but restart of SQL Server is required for the new value `"$value`" to be used (old value: `"$currentRunValue`")" -Target $Instance + } + } + catch { + Stop-Function -Message "Unable to change config setting" -Target $Instance -ErrorRecord $_ -Continue -ContinueLabel main + } + } + } + } +} +#ValidationTags#FlowControl,Pipeline# +function Set-DbaSpn { + <# +.SYNOPSIS +Sets an SPN for a given service account in active directory (and also enables delegation to the same SPN by default) + +.DESCRIPTION +This function will connect to Active Directory and search for an account. If the account is found, it will attempt to add an SPN. Once the SPN +is added, the function will also set delegation to that service, unless -NoDelegation is specified. In order to run this function, the credential you provide must have write +access to Active Directory. + +Note: This function supports -WhatIf + +.PARAMETER SPN +The SPN you want to add + +.PARAMETER ServiceAccount +The account you want the SPN added to + +.PARAMETER Credential +The credential you want to use to connect to Active Directory to make the changes + +.PARAMETER NoDelegation +Skips setting the delegation + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Confirm +Turns confirmations before changes on or off + +.PARAMETER WhatIf +Shows what would happen if the command was executed + +.NOTES +Tags: SPN +Author: Drew Furgiuele (@pittfurg), http://www.port1433.com + +dbatools PowerShell module (https://dbatools.io) +Copyright (C) 2016 Chrissy LeMaire +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaSpn + +.EXAMPLE +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account + +Connects to Active Directory and adds a provided SPN to the given account. + +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -EnableException + +Connects to Active Directory and adds a provided SPN to the given account, suppressing all error messages and throw exceptions that can be caught instead + +.EXAMPLE +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -Credential (Get-Credential) + +Connects to Active Directory and adds a provided SPN to the given account. Uses alternative account to connect to AD. + +.EXAMPLE +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -NoDelegation + +Connects to Active Directory and adds a provided SPN to the given account, without the delegation. + +.EXAMPLE +Test-DbaSpn -ComputerName sql2016 | Where { $_.isSet -eq $false } | Set-DbaSpn + +Sets all missing SPNs for sql2016 + +.EXAMPLE +Test-DbaSpn -ComputerName sql2016 | Where { $_.isSet -eq $false } | Set-DbaSpn -WhatIf + +Displays what would happen trying to set all missing SPNs for sql2016 + +#> + [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("RequiredSPN")] + [string]$SPN, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("InstanceServiceAccount", "AccountName")] + [string]$ServiceAccount, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] + [PSCredential]$Credential, + [switch]$NoDelegation, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + #did we find the server account? + Write-Message -Message "Looking for account $ServiceAccount..." -Level Verbose + $searchfor = 'User' + if ($ServiceAccount.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $ServiceAccount -Type $searchfor -Credential $Credential -EnableException + } + catch { + Stop-Function -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($ServiceAccount). $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + if ($Result.Count -gt 0) { + try { + $adentry = $Result.GetUnderlyingObject() + } + catch { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has been found, but you don't have enough permission to inspect its properties $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + } + else { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has not been found" -EnableException $EnableException -Target $ServiceAccount + } + # Cool! Add an SPN + $delegate = $true + if ($PSCmdlet.ShouldProcess("$spn", "Adding SPN to service account")) { + try { + $null = $adentry.Properties['serviceprincipalname'].Add($spn) + $status = "Successfully added SPN" + $adentry.CommitChanges() + Write-Message -Message "Added SPN $spn to $ServiceAccount" -Level Verbose + $set = $true + } + catch { + Write-Message -Message "Could not add SPN. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $false + $status = "Failed to add SPN" + $delegate = $false + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "servicePrincipalName" + IsSet = $set + Notes = $status + } + } + + #if we have the SPN set, we can add the delegation + if ($delegate) { + # but only if $NoDelegation is not passed + if (!$NoDelegation) { + if ($PSCmdlet.ShouldProcess("$spn", "Adding constrained delegation to service account for SPN")) { + try { + $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Add($spn) + $adentry.CommitChanges() + Write-Message -Message "Added kerberos delegation to $spn for $ServiceAccount" -Level Verbose + $set = $true + $status = "Successfully added constrained delegation" + } + catch { + Write-Message -Message "Could not add delegation. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $false + $status = "Failed to add constrained delegation" + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $set + Notes = $status + } + } + } + else { + Write-Message -Message "Skipping delegation as instructed" -Level Verbose + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaStartupParameter { + <# + .SYNOPSIS + Sets the Startup Parameters for a SQL Server instance + + .DESCRIPTION + Modifies the startup parameters for a specified SQL Server Instance + + For full details of what each parameter does, please refer to this MSDN article - https://msdn.microsoft.com/en-us/library/ms190737(v=sql.105).aspx + + .PARAMETER SqlInstance + The SQL Server instance to be modified + + If the Sql Instance is offline path parameters will be ignored as we cannot test the instance's access to the path. If you want to force this to work then please use the Force switch + + .PARAMETER SqlCredential + Windows or Sql Login Credential with permission to log into the SQL instance + + .PARAMETER Credential + Windows Credential with permission to log on to the server running the SQL instance + + .PARAMETER MasterData + Path to the data file for the Master database + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER MasterLog + Path to the log file for the Master database + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER ErrorLog + Path to the SQL Server error log file + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER TraceFlags + A comma separated list of TraceFlags to be applied at SQL Server startup + By default these will be appended to any existing trace flags set + + .PARAMETER CommandPromptStart + Shortens startup time when starting SQL Server from the command prompt. Typically, the SQL Server Database Engine starts as a service by calling the Service Control Manager. + Because the SQL Server Database Engine does not start as a service when starting from the command prompt + + .PARAMETER MinimalStart + Starts an instance of SQL Server with minimal configuration. This is useful if the setting of a configuration value (for example, over-committing memory) has + prevented the server from starting. Starting SQL Server in minimal configuration mode places SQL Server in single-user mode + + .PARAMETER MemoryToReserve + Specifies an integer number of megabytes (MB) of memory that SQL Server will leave available for memory allocations within the SQL Server process, + but outside the SQL Server memory pool. The memory outside of the memory pool is the area used by SQL Server for loading items such as extended procedure .dll files, + the OLE DB providers referenced by distributed queries, and automation objects referenced in Transact-SQL statements. The default is 256 MB. + + .PARAMETER SingleUser + Start Sql Server in single user mode + + .PARAMETER NoLoggingToWinEvents + Don't use Windows Application events log + + .PARAMETER StartAsNamedInstance + Allows you to start a named instance of SQL Server + + .PARAMETER DisableMonitoring + Disables the following monitoring features: + + SQL Server performance monitor counters + Keeping CPU time and cache-hit ratio statistics + Collecting information for the DBCC SQLPERF command + Collecting information for some dynamic management views + Many extended-events event points + + ** Warning *\* When you use the -x startup option, the information that is available for you to diagnose performance and functional problems with SQL Server is greatly reduced. + + .PARAMETER SingleUserDetails + The username for single user + + .PARAMETER IncreasedExtents + Increases the number of extents that are allocated for each file in a filegroup. + + .PARAMETER TraceFlagsOverride + Overrides the default behaviour and replaces any existing trace flags. If not trace flags specified will just remove existing ones + + .PARAMETER StartUpConfig + Pass in a previously saved SQL Instance startup config + using this parameter will set TraceFlagsOverride to true, so existing Trace Flags will be overridden + + .PARAMETER Offline + Setting this switch will try perform the requested actions without connect to the SQL Server Instance, this will speed things up if you know the Instance is offline. + + When working offline, path inputs (MasterData, MasterLog and ErrorLog) will be ignored, unless Force is specified + + .PARAMETER Force + By default we test the values passed in via MasterData, MasterLog, ErrorLog + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Service, Startup, Parameter, Configure + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser + + Will configure the SQL Instance server1\instance1 to startup up in Single User mode at next startup + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -IncreasedExtents + + Will configure the SQL Instance sql2016 to IncreasedExtents = True (-E) + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -IncreasedExtents:$false -WhatIf + + Shows what would happen if you attempted to configure the SQL Instance sql2016 to IncreasedExtents = False (no -E) + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -TraceFlags 8032,8048 + + This will append Trace Flags 8032 and 8048 to the startup parameters + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -SingleUser:$false -TraceFlagsOverride + + This will remove all trace flags and set SinguleUser to false + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -TraceFlags 8032,8048 -TraceFlagsOverride + + This will set Trace Flags 8032 and 8048 to the startup parameters, removing any existing Trace Flags + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -SingleUser:$false -TraceFlagsOverride -Offline + + This will remove all trace flags and set SinguleUser to false from an offline instance + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -ErrorLog c:\Sql\ -Offline + + This will attempt to change the ErrorLog path to c:\sql\. However, with the offline switch this will not happen. To force it, use the -Force switch like so: + + Set-DbaStartupParameter -SqlInstance sql2016 -ErrorLog c:\Sql\ -Offline -Force + + .EXAMPLE + $StartupConfig = Get-DbaStartupParameter -SqlInstance server1\instance1 + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -NoLoggingToWinEvents + #Restart your SQL instance with the tool of choice + #Do Some work + Set-DbaStartupParameter -SqlInstance server1\instance1 -StartUpConfig $StartUpConfig + #Restart your SQL instance with the tool of choice and you're back to normal + + In this example we take a copy of the existing startup configuration of server1\instance1 + + We then change the startup parameters ahead of some work + + After the work has been completed, we can push the original startup parameters back to server1\instance1 and resume normal operation + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ([parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [string]$MasterData, + [string]$MasterLog, + [string]$ErrorLog, + [string[]]$TraceFlags, + [switch]$CommandPromptStart, + [switch]$MinimalStart, + [int]$MemoryToReserve, + [switch]$SingleUser, + [string]$SingleUserDetails, + [switch]$NoLoggingToWinEvents, + [switch]$StartAsNamedInstance, + [switch]$DisableMonitoring, + [switch]$IncreasedExtents, + [switch]$TraceFlagsOverride, + [object]$StartUpConfig, + [switch]$Offline, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + if (-not $Offline) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $SqlInstance" -Target $SqlInstance + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to $SqlInstance, will try to work with just WMI. Path options will be ignored unless Force was indicated" + $Server = $SqlInstance + $Offline = $true + } + } + else { + Write-Message -Level Verbose -Message "Offline switch set, proceeding with just WMI" + $Server = $SqlInstance + } + + #Get Current parameters: + $currentstartup = Get-DbaStartupParameter -SqlInstance $server -Credential $Credential + $originalparamstring = $currentstartup.ParameterString + + Write-Message -Level Output -Message "Original startup parameter string: $originalparamstring" + + if ('startUpconfig' -in $PsBoundParameters.keys) { + Write-Message -Level VeryVerbose -Message "StartupObject passed in" + $newstartup = $StartUpConfig + $TraceFlagsOverride = $true + } + else { + Write-Message -Level VeryVerbose -Message "Parameters passed in" + $newstartup = $currentstartup.PSObject.copy() + foreach ($param in ($PsBoundParameters.keys | Where-Object { $_ -in ($newstartup.PSObject.Properties.name) })) { + if ($PsBoundParameters.item($param) -ne $newstartup.$param) { + $newstartup.$param = $PsBoundParameters.item($param) + } + } + } + + if (!($currentstartup.SingleUser)) { + + if ($newstartup.Masterdata.length -gt 0) { + if ($Offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested MasterData path" + $ParameterString += "-d$($CurrentStartup.MasterData);" + + } + else { + if ($Force) { + $ParameterString += "-d$($newstartup.MasterData);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.MasterData -Parent)) { + $ParameterString += "-d$($newstartup.MasterData);" + } + else { + Stop-Function -Message "Specified folder for Master Data file is not reachable by instance $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "MasterData value must be provided" + return + } + + if ($newstartup.ErrorLog.length -gt 0) { + if ($Offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested ErrorLog path" + $ParameterString += "-e$($CurrentStartup.ErrorLog);" + } + else { + if ($Force) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.ErrorLog -Parent)) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + else { + Stop-Function -Message "Specified folder for ErrorLog file is not reachable by $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "ErrorLog value must be provided" + return + } + + if ($newstartup.MasterLog.Length -gt 0) { + if ($offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested MasterLog path" + $ParameterString += "-l$($CurrentStartup.MasterLog);" + } + else { + if ($Force) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.MasterLog -Parent)) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + else { + Stop-Function -Message "Specified folder for Master Log file is not reachable by $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "MasterLog value must be provided." + return + } + } + else { + + Write-Message -Level Verbose -Message "Sql instance is presently configured for single user, skipping path validation" + if ($newstartup.MasterData.Length -gt 0) { + $ParameterString += "-d$($newstartup.MasterData);" + } + else { + Stop-Function -Message "Must have a value for MasterData" + return + } + if ($newstartup.ErrorLog.Length -gt 0) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + else { + Stop-Function -Message "Must have a value for Errorlog" + return + } + if ($newstartup.MasterLog.Length -gt 0) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + else { + Stop-Function -Message "Must have a value for MsterLog" + return + } + } + + if ($newstartup.CommandPromptStart) { + $ParameterString += "-c;" + } + if ($newstartup.MinimalStart) { + $ParameterString += "-f;" + } + if ($newstartup.MemoryToReserve -notin ($null, 0)) { + $ParameterString += "-g$($newstartup.MemoryToReserve)" + } + if ($newstartup.SingleUser) { + if ($SingleUserDetails.length -gt 0) { + if ($SingleUserDetails -match ' ') { + $SingleUserDetails = """$SingleUserDetails""" + } + $ParameterString += "-m$SingleUserDetails;" + } + else { + $ParameterString += "-m;" + } + } + if ($newstartup.NoLoggingToWinEvents) { + $ParameterString += "-n;" + } + If ($newstartup.StartAsNamedInstance) { + $ParameterString += "-s;" + } + if ($newstartup.DisableMonitoring) { + $ParameterString += "-x;" + } + if ($newstartup.IncreasedExtents) { + $ParameterString += "-E;" + } + if ($newstartup.TraceFlags -eq 'None') { + $newstartup.TraceFlags = '' + } + if ($TraceFlagsOverride -and 'TraceFlags' -in $PsBoundParameters.keys) { + if ($null -ne $TraceFlags -and '' -ne $TraceFlags) { + $newstartup.TraceFlags = $TraceFlags -join ',' + $ParameterString += (($TraceFlags.split(',') | ForEach-Object { "-T$_" }) -join ';') + ";" + } + } + else { + if ('TraceFlags' -in $PsBoundParameters.keys) { + if ($null -eq $TraceFlags) { $TraceFlags = '' } + $oldflags = @($currentstartup.TraceFlags) -split ',' | Where-Object { $_ -ne 'None' } + $newflags = $TraceFlags + $newstartup.TraceFlags = (@($oldFlags) + @($newflags) | Sort-Object -Unique) -join ',' + } + elseif ($TraceFlagsOverride) { + $newstartup.TraceFlags = '' + } + else { + $newstartup.TraceFlags = if ($currentstartup.TraceFlags -eq 'None') { } + else { $currentstartup.TraceFlags -join ',' } + } + If ($newstartup.TraceFlags.Length -ne 0) { + $ParameterString += (($newstartup.TraceFlags.split(',') | ForEach-Object { "-T$_" }) -join ';') + ";" + } + } + + $instance = $SqlInstance.ComputerName + $instancename = $SqlInstance.InstanceName + Write-Message -Level Verbose -Message "Attempting to connect to $instancename on $instance" + + if ($instancename.Length -eq 0) { $instancename = "MSSQLSERVER" } + + $displayname = "SQL Server ($instancename)" + + if ($originalparamstring -eq "$ParameterString" -or "$originalparamstring;" -eq "$ParameterString") { + Stop-Function -Message "New parameter string would be the same as the old parameter string. Nothing to do." -Target $ParameterString + return + } + + $Scriptblock = { + $instance = $args[0] + $displayname = $args[1] + $ParameterString = $args[2] + + $wmisvc = $wmi.Services | Where-Object { $_.DisplayName -eq $displayname } + $wmisvc.StartupParameters = $ParameterString + $wmisvc.Alter() + $wmisvc.Refresh() + if ($wmisvc.StartupParameters -eq $ParameterString) { + $true + } + else { + $false + } + } + + if ($pscmdlet.ShouldProcess("Setting Sql Server start parameters on $SqlInstance to $ParameterString")) { + try { + if ($Credential) { + $response = Invoke-ManagedComputerCommand -ComputerName $instance -Credential $Credential -ScriptBlock $Scriptblock -ArgumentList $instance, $displayname, $ParameterString -EnableException + $output = Get-DbaStartupParameter -SqlInstance $server -Credential $Credential -EnableException + Add-Member -Force -InputObject $output -MemberType NoteProperty -Name OriginalStartupParameters -Value $originalparamstring + } + else { + $response = Invoke-ManagedComputerCommand -ComputerName $instance -ScriptBlock $Scriptblock -ArgumentList $instance, $displayname, $ParameterString -EnableException + $output = Get-DbaStartupParameter -SqlInstance $server -EnableException + Add-Member -Force -InputObject $output -MemberType NoteProperty -Name OriginalStartupParameters -Value $originalparamstring + } + + $output + + Write-Message -Level Output -Message "Startup parameters changed on $SqlInstance. You must restart SQL Server for changes to take effect." + } + catch { + Stop-Function -Message "Startup parameters failed to change on $SqlInstance. " -Target $SqlInstance -ErrorRecord $_ + return + } + } + } +} +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaTcpPort { + <# + .SYNOPSIS + Changes the TCP port used by the specified SQL Server. + + .DESCRIPTION + This function changes the TCP port used by the specified SQL Server. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server instance as a different user + + .PARAMETER Credential + Credential object used to connect to the Windows server itself as a different user + + .PARAMETER Port + TCPPort that SQLService should listen on. + + .PARAMETER IpAddress + Wich IpAddress should the portchange , if omitted allip (0.0.0.0) will be changed with the new portnumber. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: Service, Port, TCP, Configure + Author: Hansson7707@gmail.com, @H0s0n77 + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaTcpPort + + .EXAMPLE + Set-DbaTcpPort -SqlInstance SqlInstance2014a -Port 1433 + + Sets the port number 1433 for allips on the default instance on SqlInstance2014a + + .EXAMPLE + Set-DbaTcpPort -SqlInstance winserver\sqlexpress -IpAddress 192.168.1.22 -Port 1433 + + Sets the port number 1433 for IP 192.168.1.22 on the sqlexpress instance on winserver + + .EXAMPLE + Set-DbaTcpPort -SqlInstance 'SQLDB2014A' ,'SQLDB2016B' -port 1337 + + Sets the port number 1337 for ALLIP's on SqlInstance SQLDB2014A and SQLDB2016B + #> + [CmdletBinding(ConfirmImpact = "High")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [parameter(Mandatory = $true)] + [ValidateRange(1, 65535)] + [int]$Port, + [IpAddress[]]$IpAddress, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if ($IpAddress.Length -eq 0) { + $IpAddress = '0.0.0.0' + } + else { + if ($SqlInstance.count -gt 1) { + Stop-Function -Message "-IpAddress switch cannot be used with a collection of serveraddresses" -Target $SqlInstance + return + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $wmiinstancename = $server.ServiceName + + + if ($server.IsClustered) { + Write-Message -Level Verbose -Message "Instance is clustered fetching nodes..." + $clusternodes = (Get-DbaClusterNode -SqlInstance $server).ComputerName -join ", " + + Write-Message -Level Output -Message "$instance is a clustered instance, portchanges will be reflected on all nodes ($clusternodes) after a failover" + } + + $scriptblock = { + $instance = $args[0] + $wmiinstancename = $args[1] + $port = $args[2] + $IpAddress = $args[3] + $sqlinstanceName = $args[4] + + $wmi = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer $instance + $wmiinstance = $wmi.ServerInstances | Where-Object { $_.Name -eq $wmiinstancename } + $tcp = $wmiinstance.ServerProtocols | Where-Object { $_.DisplayName -eq 'TCP/IP' } + $IpAddress = $tcp.IpAddresses | where-object { $_.IpAddress -eq $IpAddress } + $tcpport = $IpAddress.IpAddressProperties | Where-Object { $_.Name -eq 'TcpPort' } + + $oldport = $tcpport.Value + try { + $tcpport.value = $port + $tcp.Alter() + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $wmiinstancename + SqlInstance = $sqlinstanceName + OldPortNumber = $oldport + PortNumber = $Port + Status = "Success" + } + } + catch { + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $wmiinstancename + SqlInstance = $sqlinstanceName + OldPortNumber = $oldport + PortNumber = $Port + Status = "Failed: $_" + } + } + } + + try { + $computerName = $instance.ComputerName + $resolved = Resolve-DbaNetworkName -ComputerName $computerName + + Write-Message -Level Verbose -Message "Writing TCPPort $port for $instance to $($resolved.FQDN)..." + Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock $scriptblock -ArgumentList $Server.NetName, $wmiinstancename, $port, $IpAddress, $server.DomainInstanceName -Credential $Credential + + } + catch { + Invoke-ManagedComputerCommand -ComputerName $instance.ComputerName -ScriptBlock $scriptblock -ArgumentList $Server.NetName, $wmiinstancename, $port, $IpAddress, $server.DomainInstanceName -Credential $Credential + } + } + } +} +function Set-DbaTempDbConfiguration { + <# + .SYNOPSIS + Sets tempdb data and log files according to best practices. + + .DESCRIPTION + Calculates tempdb size and file configurations based on passed parameters, calculated values, and Microsoft best practices. User must declare SQL Server to be configured and total data file size as mandatory values. Function then calculates the number of data files based on logical cores on the target host and create evenly sized data files based on the total data size declared by the user, with a log file 25% of the total data file size. + + Other parameters can adjust the settings as the user desires (such as different file paths, number of data files, and log file size). No functions that shrink or delete data files are performed. If you wish to do this, you will need to resize tempdb so that it is "smaller" than what the function will size it to before running the function. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DataFileCount + Specifies the number of data files to create. If this number is not specified, the number of logical cores of the host will be used. + + .PARAMETER DataFileSizeMB + Specifies the total data file size in megabytes. This is distributed across the total number of data files. + + .PARAMETER LogFileSizeMB + Specifies the log file size in megabytes. If not specified, this will be set to 25% of total data file size. + + .PARAMETER DataFileGrowthMB + Specifies the growth amount for the data file(s) in megabytes. The default is 512 MB. + + .PARAMETER LogFileGrowthMB + Specifies the growth amount for the log file in megabytes. The default is 512 MB. + + .PARAMETER DataPath + Specifies the filesystem path in which to create the tempdb data files. If not specified, current tempdb location will be used. + + .PARAMETER LogPath + Specifies the filesystem path in which to create the tempdb log file. If not specified, current tempdb location will be used. + + .PARAMETER OutputScriptOnly + If this switch is enabled, only the T-SQL script to change the tempdb configuration is created and output. + + .PARAMETER OutFile + Specifies the filesystem path into which the generated T-SQL script will be saved. + + .PARAMETER DisableGrowth + If this switch is enabled, the tempdb files will be configured to not grow. This overrides -DataFileGrowthMB and -LogFileGrowthMB. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Tempdb, Space, Configure, Configuration + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaTempDbConfiguration + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 + + Creates tempdb with a number of data files equal to the logical cores where each file is equal to 1000MB divided by the number of logical cores, with a log file of 250MB. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -DataFileCount 8 + + Creates tempdb with 8 data files, each one sized at 125MB, with a log file of 250MB. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -OutputScriptOnly + + Provides a SQL script output to configure tempdb according to the passed parameters. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -DisableGrowth + + Disables the growth for the data and log files. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -OutputScriptOnly + + Returns the T-SQL script representing tempdb configuration. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [int]$DataFileCount, + [Parameter(Mandatory = $true)] + [int]$DataFileSizeMB, + [int]$LogFileSizeMB, + [int]$DataFileGrowthMB = 512, + [int]$LogFileGrowthMB = 512, + [string]$DataPath, + [string]$LogPath, + [string]$OutFile, + [switch]$OutputScriptOnly, + [switch]$DisableGrowth, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $sql = @() + Write-Message -Level Verbose -Message "Connecting to $SqlInstance" + $server = Connect-SqlInstance $sqlinstance -SqlCredential $SqlCredential + + if ($server.VersionMajor -lt 9) { + Stop-Function -Message "SQL Server 2000 is not supported" + return + } + } + + process { + + if (Test-FunctionInterrupt) { + return + } + + $cores = $server.Processors + if ($cores -gt 8) { + $cores = 8 + } + + #Set DataFileCount if not specified. If specified, check against best practices. + if (-not $DataFileCount) { + $DataFileCount = $cores + Write-Message -Message "Data file count set to number of cores: $DataFileCount" -Level Verbose + } + else { + if ($DataFileCount -gt $cores) { + Write-Message -Message "Data File Count of $DataFileCount exceeds the Logical Core Count of $cores. This is outside of best practices." -Level Warning + } + Write-Message -Message "Data file count set explicitly: $DataFileCount" -Level Verbose + } + + $DataFilesizeSingleMB = $([Math]::Floor($DataFileSizeMB / $DataFileCount)) + Write-Message -Message "Single data file size (MB): $DataFilesizeSingleMB." -Level Verbose + + if ($DataPath) { + if ((Test-DbaSqlPath -SqlInstance $server -Path $DataPath) -eq $false) { + Stop-Function -Message "$datapath is an invalid path." + return + } + } + else { + $Filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 1').Tables[0].Rows[0].FileName + $DataPath = Split-Path $Filepath + } + + Write-Message -Message "Using data path: $datapath." -Level Verbose + + if ($LogPath) { + if ((Test-DbaSqlPath -SqlInstance $server -Path $LogPath) -eq $false) { + Stop-Function -Message "$LogPath is an invalid path." + return + } + } + else { + $Filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 2').Tables[0].Rows[0].FileName + $LogPath = Split-Path $Filepath + } + Write-Message -Message "Using log path: $LogPath." -Level Verbose + + # Check if the file growth needs to be disabled + if ($DisableGrowth) { + $DataFileGrowthMB = 0 + $LogFileGrowthMB = 0 + } + + # Check current tempdb. Throw an error if current tempdb is larger than config. + $CurrentFileCount = $server.Databases['tempdb'].ExecuteWithResults('SELECT count(1) as FileCount FROM sys.database_files WHERE type=0').Tables[0].Rows[0].FileCount + $TooBigCount = $server.Databases['tempdb'].ExecuteWithResults("SELECT TOP 1 (size/128) as Size FROM sys.database_files WHERE size/128 > $DataFilesizeSingleMB AND type = 0").Tables[0].Rows[0].Size + + if ($CurrentFileCount -gt $DataFileCount) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb has a greater number of files ($CurrentFileCount) than the calculated configuration ($DataFileCount)." + return + } + + if ($TooBigCount) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb ($TooBigCount MB) is larger than the calculated individual file configuration ($DataFilesizeSingleMB MB)." + return + } + + $EqualCount = $server.Databases['tempdb'].ExecuteWithResults("SELECT count(1) as FileCount FROM sys.database_files WHERE size/128 = $DataFilesizeSingleMB AND type = 0").Tables[0].Rows[0].FileCount + + if ($EqualCount -gt 0) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb is the same size as the specified DataFileSizeMB." + return + } + + Write-Message -Message "tempdb configuration validated." -Level Verbose + + $DataFiles = $server.Databases['tempdb'].ExecuteWithResults("select f.name as Name, f.physical_name as FileName from sys.filegroups fg join sys.database_files f on fg.data_space_id = f.data_space_id where fg.name = 'PRIMARY' and f.type_desc = 'ROWS'").Tables[0]; + + #Checks passed, process reconfiguration + for ($i = 0; $i -lt $DataFileCount; $i++) { + $File = $DataFiles.Rows[$i] + if ($File) { + $Filename = Split-Path $File.FileName -Leaf + $LogicalName = $File.Name + $NewPath = "$datapath\$Filename" + $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$LogicalName,filename='$NewPath',size=$DataFilesizeSingleMB MB,filegrowth=$DataFileGrowthMB);" + } + else { + $NewName = "tempdev$i.ndf" + $NewPath = "$datapath\$NewName" + $sql += "ALTER DATABASE tempdb ADD FILE(name=tempdev$i,filename='$NewPath',size=$DataFilesizeSingleMB MB,filegrowth=$DataFileGrowthMB);" + } + } + + if (-not $LogFileSizeMB) { + $LogFileSizeMB = [Math]::Floor($DataFileSizeMB / 4) + } + + $logfile = $server.Databases['tempdb'].ExecuteWithResults("SELECT name, physical_name as FileName FROM sys.database_files WHERE file_id = 2").Tables[0].Rows[0]; + $Filename = Split-Path $logfile.FileName -Leaf + $LogicalName = $logfile.Name + $NewPath = "$LogPath\$Filename" + $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$LogicalName,filename='$NewPath',size=$LogFileSizeMB MB,filegrowth=$LogFileGrowthMB);" + + Write-Message -Message "SQL Statement to resize tempdb." -Level Verbose + Write-Message -Message ($sql -join "`n`n") -Level Verbose + + if ($OutputScriptOnly) { + return $sql + } + elseif ($OutFile) { + $sql | Set-Content -Path $OutFile + } + else { + if ($Pscmdlet.ShouldProcess($SqlInstance, "Executing query and informing that a restart is required.")) { + try { + $server.Databases['master'].ExecuteNonQuery($sql) + Write-Message -Level Verbose -Message "tempdb successfully reconfigured." + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DataFileCount = $DataFileCount + DataFileSizeMB = $DataFileSizeMB + SingleDataFileSizeMB = $DataFilesizeSingleMB + LogSizeMB = $LogFileSizeMB + DataPath = $DataPath + LogPath = $LogPath + DataFileGrowthMB = $DataFileGrowthMB + LogFileGrowthMB = $LogFileGrowthMB + } + + Write-Message -Level Output -Message "tempdb reconfigured. You must restart the SQL Service for settings to take effect." + } + catch { + Stop-Function -Message "Unable to reconfigure tempdb. Exception: $_" -Target $sql -InnerErrorRecord $_ + return + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Set-SqlTempDbConfiguration + } +} +function Show-DbaDatabaseList { + <# + .SYNOPSIS + Shows a list of databases in a GUI. + + .DESCRIPTION + Shows a list of databases in a GUI. Returns a string holding the name of the selected database. Hitting cancel returns null. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to.. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Title + Title of the window being displayed. Default is "Select Database". + + .PARAMETER Header + Header text displayed above the database listing. Default is "Select the database:". + + .PARAMETER DefaultDb + Specify a database to have selected when the window appears. + + .NOTES + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Show-DbaDatabaseList + + .EXAMPLE + Show-DbaDatabaseList -SqlInstance sqlserver2014a + + Shows a GUI list of databases using Windows Authentication to connect to the SQL Server. Returns a string of the selected database. + + .EXAMPLE + Show-DbaDatabaseList -Source sqlserver2014a -SqlCredential $cred + + Shows a GUI list of databases using SQL credentials to connect to the SQL Server. Returns a string of the selected database. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Title = "Select Database", + [string]$Header = "Select the database:", + [string]$DefaultDb + ) + + begin { + try { + Add-Type -AssemblyName PresentationFramework + } + catch { + throw "Windows Presentation Framework required but not installed" + } + + function Add-TreeItem { + Param ( + [string]$name, + [object]$parent, + [string]$tag + ) + + $childitem = New-Object System.Windows.Controls.TreeViewItem + $textblock = New-Object System.Windows.Controls.TextBlock + $textblock.Margin = "5,0" + $stackpanel = New-Object System.Windows.Controls.StackPanel + $stackpanel.Orientation = "Horizontal" + $image = New-Object System.Windows.Controls.Image + $image.Height = 20 + $image.Width = 20 + $image.Stretch = "Fill" + $image.Source = $dbicon + $textblock.Text = $name + $childitem.Tag = $name + + if ($name -eq $DefaultDb) { + $childitem.IsSelected = $true + $script:selected = $name + } + + [void]$stackpanel.Children.Add($image) + [void]$stackpanel.Children.Add($textblock) + + $childitem.Header = $stackpanel + [void]$parent.Items.Add($childitem) + } + + function Convert-b64toimg { + param ($base64) + + $bitmap = New-Object System.Windows.Media.Imaging.BitmapImage + $bitmap.BeginInit() + $bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64) + $bitmap.EndInit() + $bitmap.Freeze() + return $bitmap + } + + $dbicon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAFRSURBVDhPY/j//z9VMVZBSjCCgQZunFn6/8zenv+7llf83zA75/+6WTn/N80v+L93ddP/M/tnY2jAayDIoNvn5/5/cX/t/89vdv7/9fUQGIPYj2+t/H/xyJT/O1ZUoWjCaeCOxcX///48ShSeWhMC14jXwC9Xs/5/fzHr/6/PW+GaQS78/WH9/y+Pe8DyT3fYEmcgKJw+HHECawJp/vZ60f8v95v/fzgd8P/tVtn/L1cw/n+0iOH/7TlMxBkIigBiDewr9iVsICg2qWrg6qnpA2dgW5YrYQOX9icPAQPfU9PA2S2RRLuwMtaGOAOf73X+//FyGl4DL03jIM5AEFjdH/x//+Lo/1cOlP9/dnMq2MA3x/z/312l/P/4JNH/axoU/0/INUHRhNdAEDi+pQ1cZIFcDEpvoPCaVOTwf1Gjy/9ds5MxNGAYSC2MVZB8/J8BAGcHwqQBNWHRAAAAAElFTkSuQmCC" + $foldericon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAHaSURBVDhPY/j//z9VMVZBSjBWQUowVkFKMApnzZL+/+gYWZ4YDGeANL95sun/j3fbwPjbm5X/Pz+cRLKhcAayq2B45YKe/8vndoHx4lltYLxgajMKhumHYRQDf37Yh4J/fNry//fb1f9/v1n6/8/Tqf//3O/6/+dO9f9fV4v+fzmV/v/L0aj/lflJQO1YDAS5AmwI1MvfPyAZ9KgbYtDlvP/fzyT9/3w45P+HPT7/z8+UwG0gyDvIBmIYBnQVyDCQq0CGPV9p8v94P/f/rKQwoHYsBs4HhgfIQJjLfr+YjdOwt5tt/z9eov1/fxf3/+ggD6B2HAaCXQYKM6hhv+81oYQXzLCXq03/P5qn/H9LE/9/LycroHYsBs7oq4EYCDIM6FVshr3Z4gg2DOS6O9Nk/q+sFvlvZawD1I7FwKldleC0h2zY9wuZEMP2+aMYdn+W/P/rE0T/zy+T+q+jJg/UjsXASe1l/z/cX/T/1dn8/492ePy/vc7s/82VOv8vLVT9f3yGwv89ffL/1zXL/l9dJwF2GciwaYVy/xVlxIDasRjY31Lyv7Uy+39ZTvz/1JiA/8Hejv8dLA3+62sqgTWJC/HixDAzQBjOoBbGKkgJxipICcYqSD7+zwAAkIiWzSGuSg0AAAAASUVORK5CYII=" + $dbatoolsicon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAO9SURBVEhL3VVdTFNXHO9MzPTF+OzDeBixFdTMINIWsAUK3AIVkFvAIQVFRLYZKR8Wi1IEKV9DYB8PGFAyEx8QScySabYY5+I2JvK18iWISKGk0JGhLzA3+e2c29uHtpcvH/0lv9yennN+v3vO/3fOFb2fCAg4vXWPNOmMRJ745TtTSskqeElviGXJ0XtkWvjJkyGLPoFAVQZoe/NkX/n6Mh/ysu4Qy7WZdJAutxRW6zT6LcNQaE4LiGgREH4cibpCMNqzCIk9hbScEoSSZ0zKOa7fRxG/k5d1h8ukvO4a5ubmMT1jw5E0vZcBZWzqOTS3dcB8tRXZeRX4/v5DZH5uIu0Wrn8NEzaNDjgYoUPd120oMjViX2iql8H6ZFd8DzE7eFl3iOWpuyQydlh44kbJroilSd8RuQ+cqh7wC9Z+JJaxY8KTN0gp+5Yk9DaREzYhb5FOBwZFZ6LlZifKa5ux//AxYTHCvSEp8A9O5n77B6dwqXS119guZ+GrGq9jfn4eM7ZZxB/PdxN2UfOpHq3kRWq/uoE8Yx3u/fQLzhSYUdN0g+tfN126z0oxNj6BJz0Dq0b4E2UawuJzuPhKyZmKYr/AocgMrk37VzWRBLGRdE/psuXqk9wkT/GNUCJLWqS3By/rDh9FxjaSrnahiZ7cq8wCUzKImLIJqC+Ngbk4gmjjIKKKB6Aq7l+OLBmfVF0YnlQZR1p4eSd2y5IiyEr+oyJ0CwIi0gUNKAOPmnG04Q0utf+DHweWkFjjQOyVWajLpsCUPkeUcRgqAzE09Dfz8k64aqI9YcDziUk87bMgOCZL0CQ0ux2J9UtIbXyFwall/PD0NeLKrU6DkhGymj8RXtRDjU7x8k64TKpJQmi6bLOzSEgv8DYhNWMujiK+9jU0VQs4Vm/H2MwSOh4vcP+rii2cQVh+F+IqbRJe3glyReuoSFBUJtpu3eWulv2h3ueE1iOu0g5N9QL3jLk8jerbdrz59y1yGoYQUdSLsII/CLscIsD9UPrLUz4myXhBhWjCPMVdPBBnhMbsIAZzSDDbcOvRIhyLy6i4+Qyq82QFxECR9xjK/K5OXtodNHo+CsW2tagunbxADbK+sXP16Bv/G7lNQ8hpHEX21UGoDb/j8NmfoSzoNvCymwdTPvMotsKGB32LaL1H0mS0oOHOFLpH/0L3iAOF3/YSk4dgTBMh/JTNgdVbtzNl1il12UuSpHE+SRayTb0IL3yCMP2vUJKtUuh/szNNK8Jfxw3BZNpiMoGjiKPJm54Ffw8gEv0PQRYX7wDAUKEAAAAASUVORK5CYII=" + + $sourceserver = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + + process { + # Create XAML form in Visual Studio, ensuring the ListView looks chromeless + [xml]$xaml = " + + +
[^\]]+)\] (?[^\ ]+)( PARTITION = (?\d+))? WITH \((?[^\)]+)') { + $fresh['Index'] = $Matches.index + $fresh['Statistics'] = $null + $fresh['Schema'] = $Matches.Schema + $fresh['Table'] = $Matches.Table + $fresh['Action'] = $Matches.action + $fresh['Options'] = $Matches.options + $fresh['Timeout'] = $Matches.timeout + $fresh['Partition'] = $Matches.partition + } + elseif ($fresh['Command'] -match '(SET LOCK_TIMEOUT (?\d+); )?UPDATE STATISTICS \[(?[^\]]+)\]\.\[(?[^]]+)\]\.\[(?
[^\]]+)\] \[(?[^\]]+)\]') { + $fresh['Index'] = $null + $fresh['Statistics'] = $Matches.stat + $fresh['Schema'] = $Matches.Schema + $fresh['Table'] = $Matches.Table + $fresh['Action'] = $null + $fresh['Options'] = $null + $fresh['Timeout'] = $Matches.timeout + $fresh['Partition'] = $null + } + } + if ($fresh.ContainsKey('Comment')) { + $commentparts = $fresh['Comment'] -split ', ' + foreach ($part in $commentparts) { + $indkey, $indvalue = $part -split ': ', 2 + if ($fresh.ContainsKey($indkey)) { + $fresh[$indkey] = $indvalue + } + } + } + if ($null -ne $fresh['Error']) { + $fresh['Error'] = $fresh['Error'] -join "`n" + } + + return $fresh + } + } + process { + foreach ($instance in $sqlinstance) { + $logdir = $logfiles = $null + $computername = $instance.ComputerName + Write-Message -Level Verbose -Message "Connecting to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Can't connect to $instance" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($logtype -ne 'IndexOptimize') { + Write-Message -Level Warning -Message "Parsing $logtype is not supported at the moment" + Continue + } + if ($Path) { + $logdir = Join-AdminUnc -Servername $server.netname -Filepath $Path + } + else { + $logdir = Join-AdminUnc -Servername $server.netname -Filepath $server.errorlogpath # -replace '^(.):', "\\$computername\`$1$" + } + if (!$logdir) { + Write-Message -Level Warning -Message "No log directory returned from $instance" + Continue + } + + Write-Message -Level Verbose -Message "Log directory on $computername is $logdir" + if (! (Test-Path $logdir)) { + Write-Message -Level Warning -Message "Directory $logdir is not accessible" + continue + } + $logfiles = [System.IO.Directory]::EnumerateFiles("$logdir", "IndexOptimize_*.txt") + if ($Since) { + $filteredlogs = @() + foreach ($l in $logfiles) { + $base = $($l.Substring($l.Length - 15, 15)) + try { + $datefile = [DateTime]::ParseExact($base, 'yyyyMMdd_HHmmss', $null) + } + catch { + $datefile = Get-ItemProperty -Path $l | select -ExpandProperty CreationTime + } + if ($datefile -gt $since) { + $filteredlogs += $l + } + } + $logfiles = $filteredlogs + } + if (! $logfiles.count -ge 1) { + Write-Message -Level Warning -Message "No log files returned from $computername" + Continue + } + $instanceinfo = @{ } + $instanceinfo['ComputerName'] = $server.NetName + $instanceinfo['InstanceName'] = $server.ServiceName + $instanceinfo['SqlInstance'] = $server.Name + + foreach ($File in $logfiles) { + Write-Message -Level Verbose -Message "Reading $file" + $text = New-Object System.IO.StreamReader -ArgumentList "$File" + $block = New-Object System.Collections.ArrayList + $remember = @{} + while ($line = $text.ReadLine()) { + + $real = $line.Trim() + if ($real.Length -eq 0) { + $processed = process-block $block + if ('Procedure' -in $processed.Keys) { + $block = New-Object System.Collections.ArrayList + continue + } + if ('Database' -in $processed.Keys) { + Write-Message -Level Verbose -Message "Index and Stats Optimizations on Database $($processed.Database) on $computername" + $processed.Remove('Is accessible') + $processed.Remove('User access') + $processed.Remove('Date and time') + $processed.Remove('Standby') + $processed.Remove('Recovery Model') + $processed.Remove('Updateability') + $processed['Database'] = $processed['Database'].Trim('[]') + $remember = $processed.Clone() + } + else { + foreach ($k in $processed.Keys) { + $remember[$k] = $processed[$k] + } + $remember.Remove('Command') + $remember['StartTime'] = [dbadatetime]([DateTime]::ParseExact($remember['Date and time'] , "yyyy-MM-dd HH:mm:ss", $null)) + $remember.Remove('Date and time') + $remember['Duration'] = ($remember['Duration'] -as [timespan]) + [pscustomobject]$remember + } + $block = New-Object System.Collections.ArrayList + } + else { + $null = $block.Add($real) + } + } + $text.close() + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaMaxMemory.ps1 b/functions/Get-DbaMaxMemory.ps1 index a1934cfa9a5b..b78261543c05 100644 --- a/functions/Get-DbaMaxMemory.ps1 +++ b/functions/Get-DbaMaxMemory.ps1 @@ -1,80 +1,76 @@ -Function Get-DbaMaxMemory -{ -<# -.SYNOPSIS -Gets the 'Max Server Memory' configuration setting and the memory of the server. Works on SQL Server 2000-2014. - -.DESCRIPTION -This command retrieves the SQL Server 'Max Server Memory' configuration setting as well as the total physical installed on the server. - -.PARAMETER SqlServer -Allows you to specify a comma separated list of servers to query. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$cred = Get-Credential, then pass $cred variable to this parameter. - -Windows Authentication will be used when SqlCredential is not specified. To connect as a different Windows user, run PowerShell as that user. - -.NOTES -Tags: Memory -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaMaxMemory - -.EXAMPLE -Get-DbaMaxMemory -SqlServer sqlcluster,sqlserver2012 - -Get memory settings for all servers within the SQL Server Central Management Server "sqlcluster". - -.EXAMPLE -Get-DbaMaxMemory -SqlServer sqlcluster | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } - -Find all servers in Server Central Management Server that have 'Max Server Memory' set to higher than the total memory of the server (think 2147483647) - -#> - [CmdletBinding()] - Param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] - [Alias("ServerInstance", "SqlInstance", "SqlServers")] - [object]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential - ) - - PROCESS - { - foreach ($servername in $sqlserver) - { - Write-Verbose "Attempting to connect to $servername" - try - { - $server = Connect-SqlServer -SqlServer $servername -SqlCredential $SqlCredential - } - catch - { - Write-Warning "Can't connect to $servername or access denied. Skipping." - continue - } - - $totalmemory = $server.PhysicalMemory - - # Some servers under-report by 1MB. - if (($totalmemory % 1024) -ne 0) { $totalmemory = $totalmemory + 1 } - - [pscustomobject]@{ - Server = $server.name - TotalMB = $totalmemory - SqlMaxMB = $server.Configuration.MaxServerMemory.ConfigValue - } - } - } +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaMaxMemory { + <# + .SYNOPSIS + Gets the 'Max Server Memory' configuration setting and the memory of the server. Works on SQL Server 2000-2014. + + .DESCRIPTION + This command retrieves the SQL Server 'Max Server Memory' configuration setting as well as the total physical installed on the server. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: MaxMemory, Memory + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaMaxMemory + + .EXAMPLE + Get-DbaMaxMemory -SqlInstance sqlcluster,sqlserver2012 + + Get memory settings for all servers within the SQL Server Central Management Server "sqlcluster". + + .EXAMPLE + Get-DbaMaxMemory -SqlInstance sqlcluster | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } + + Find all servers in Server Central Management Server that have 'Max Server Memory' set to higher than the total memory of the server (think 2147483647) + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $totalMemory = $server.PhysicalMemory + + # Some servers under-report by 1MB. + if (($totalMemory % 1024) -ne 0) { + $totalMemory = $totalMemory + 1 + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TotalMB = [int]$totalMemory + SqlMaxMB = [int]$server.Configuration.MaxServerMemory.ConfigValue + } | Select-DefaultView -ExcludeProperty Server + } + } } diff --git a/functions/Get-DbaMemoryUsage.ps1 b/functions/Get-DbaMemoryUsage.ps1 index 7bbdc944e769..90786d20edbc 100644 --- a/functions/Get-DbaMemoryUsage.ps1 +++ b/functions/Get-DbaMemoryUsage.ps1 @@ -1,230 +1,226 @@ -Function Get-DbaMemoryUsage -{ -<# -.SYNOPSIS -Get amount of memory in use by *all* SQL Server components and instances +#ValidationTags#Messaging,CodeStyle# +function Get-DbaMemoryUsage { + <# + .SYNOPSIS + Get amount of memory in use by *all* SQL Server components and instances -.DESCRIPTION -Retrieves the amount of memory per performance counter. Default output includes columns Server, counter instance, counter, number of pages, memory in KB, memory in MB -SSAS and SSIS are included. - -SSRS does not have memory counters, only memory shrinks and memory pressure state. + .DESCRIPTION + Retrieves the amount of memory per performance counter. Default output includes columns Server, counter instance, counter, number of pages, memory in KB, memory in MB + SSAS and SSIS are included. -This function requires local admin role on the targeted computers. + SSRS does not have memory counters, only memory shrinks and memory pressure state. -.PARAMETER ComputerName -The Windows Server that you are connecting to. Note that this will return all instances, but Out-GridView makes it easy to filter to specific instances. + This function requires local admin role on the targeted computers. -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user + .PARAMETER ComputerName + The Windows Server that you are connecting to. Note that this will return all instances, but Out-GridView makes it easy to filter to specific instances. -.PARAMETER Simple -Shows concise information including Server name, Database name, and the date the last time backups were performed + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user -.NOTES -Tags: Memory -Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + .PARAMETER Simple + Shows concise information including Server name, Database name, and the date the last time backups were performed -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -SSIS Counters: https://msdn.microsoft.com/en-us/library/ms137622.aspx + .NOTES + Tags: Memory + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) -.LINK - https://dbatools.io/Get-DbaMemoryUsage + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT -.EXAMPLE -Get-DbaMemoryUsage -ComputerName ServerA + SSIS Counters: https://msdn.microsoft.com/en-us/library/ms137622.aspx -Returns a custom object displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB + .LINK + https://dbatools.io/Get-DbaMemoryUsage -.EXAMPLE -Get-DbaMemoryUsage -ComputerName ServerA\sql987 -Simple + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA -Returns a custom object with Server, counter instance, counter, number of pages, memory in KB, memory in MB + Returns a custom object displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB -.EXAMPLE -Get-DbaMemoryUsage -ComputerName ServerA\sql987 | Out-Gridview + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA\sql987 -Simple -Returns a gridview displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB + Returns a custom object with Server, counter instance, counter, number of pages, memory in KB, memory in MB -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("Host", "cn", "Server")] - [string[]]$ComputerName, - [PsCredential]$Credential, - [switch]$Simple - ) + .EXAMPLE + Get-DbaMemoryUsage -ComputerName ServerA\sql987 | Out-Gridview + Returns a gridview displaying Server, counter instance, counter, number of pages, memory in KB, memory in MB + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("Host", "cn", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch]$Simple, + [Alias('Silent')] + [switch]$EnableException + ) - BEGIN - { - if ($Simple) - { - $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' - $Plancounters = 'total\)\\cache pages' - $BufManpagecounters = 'Total pages' - $SSAScounters = '(\\memory usage)' - $SSIScounters = '(memory)' + begin { + if ($Simple) { + $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' + $Plancounters = 'total\)\\cache pages' + $BufManpagecounters = 'Total pages' + $SSAScounters = '(\\memory usage)' + $SSIScounters = '(memory)' } - else - { - $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' - $Plancounters = '(cache pages|procedure plan|ad hoc sql plan|prepared SQL Plan)' - $BufManpagecounters = '(Free pages|Reserved pages|Stolen pages|Total pages|Database pages|target pages|extension .* pages)' - $SSAScounters = '(\\memory )' - $SSIScounters = '(memory)' + else { + $Memcounters = '(Total Server Memory |Target Server Memory |Connection Memory |Lock Memory |SQL Cache Memory |Optimizer Memory |Granted Workspace Memory |Cursor memory usage|Maximum Workspace)' + $Plancounters = '(cache pages|procedure plan|ad hoc sql plan|prepared SQL Plan)' + $BufManpagecounters = '(Free pages|Reserved pages|Stolen pages|Total pages|Database pages|target pages|extension .* pages)' + $SSAScounters = '(\\memory )' + $SSIScounters = '(memory)' } - } - - PROCESS - { - foreach ($Computer in $ComputerName) - { - Write-Verbose "Connecting to $Computer" - $reply = Resolve-DbaNetworkName -ComputerName $Computer -Credential $Credential -ErrorAction SilentlyContinue - if ( $reply.ComputerName ) - { - $Computer = $reply.ComputerName - Write-Verbose "$Computer is up and running" - Write-Verbose "Searching for Memory Manager Counters on $Computer" - try - { - $availablecounters = (Get-Counter -ComputerName $Computer -ListSet '*sql*:Memory Manager*' -ErrorAction SilentlyContinue ).paths - (Get-Counter -ComputerName $Computer -Counter $availablecounters -ErrorAction SilentlyContinue ).countersamples | - Where-Object {$_.Path -match $Memcounters} | - ForEach-Object { - $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] - if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } - [PSCustomObject]@{ - ComputerName = $Computer - SqlInstance = $instance - CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$","")).split(':')[1] - Counter = $_.Path.split("\")[-1] - Pages = $null - MemKB = $_.cookedvalue - MemMB = $_.cookedvalue / 1024 - } - } + $scriptblock = { + param ($Memcounters, + $Plancounters, + $BufManpagecounters, + $SSAScounters, + $SSIScounters) + Write-Verbose "Searching for Memory Manager Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet '*sql*:Memory Manager*' -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $Memcounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue + MemMB = $_.cookedvalue / 1024 + } } - catch - { + } + catch { Write-Verbose "No Memory Manager Counters on $Computer" + } + + Write-Verbose "Searching for Plan Cache Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet '*sql*:Plan Cache*' -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $Plancounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $_.cookedvalue + MemKB = $_.cookedvalue * 8192 / 1024 + MemMB = $_.cookedvalue * 8192 / 1048576 + } } - - Write-Verbose "Searching for Plan Cache Counters on $Computer" - try - { - $availablecounters = (Get-Counter -ComputerName $Computer -ListSet '*sql*:Plan Cache*' -ErrorAction SilentlyContinue ).paths - (Get-Counter -ComputerName $Computer -Counter $availablecounters -ErrorAction SilentlyContinue ).countersamples | - Where-Object {$_.Path -match $Plancounters} | - ForEach-Object { - $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] - if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } - [PSCustomObject]@{ - ComputerName = $Computer - SqlInstance = $instance - CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$","")).split(':')[1] - Counter = $_.Path.split("\")[-1] - Pages = $_.cookedvalue - MemKB = $_.cookedvalue * 8192 / 1024 - MemMB = $_.cookedvalue * 8192 / 1048576 - } - } - } - catch - { + } + catch { Write-Verbose "No Plan Cache Counters on $Computer" + } + + Write-Verbose "Searching for Buffer Manager Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "*Buffer Manager*" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $BufManpagecounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $_.cookedvalue + MemKB = $_.cookedvalue * 8192 / 1024.0 + MemMB = $_.cookedvalue * 8192 / 1048576.0 + } } - - Write-Verbose "Searching for Buffer Manager Counters on $Computer" - try - { - $availablecounters = (Get-Counter -ComputerName $Computer -ListSet "*Buffer Manager*" -ErrorAction SilentlyContinue ).paths - (Get-Counter -ComputerName $Computer -Counter $availablecounters -ErrorAction SilentlyContinue ).countersamples | - Where-Object {$_.Path -match $BufManpagecounters} | - ForEach-Object { - $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] - if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } - [PSCustomObject]@{ - ComputerName = $Computer - SqlInstance = $instance - CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$","")).split(':')[1] - Counter = $_.Path.split("\")[-1] - Pages = $_.cookedvalue - MemKB = $_.cookedvalue * 8192 / 1024.0 - MemMB = $_.cookedvalue * 8192 /1048576.0 - } - } - } - catch - { + } + catch { Write-Verbose "No Buffer Manager Counters on $Computer" + } + + Write-Verbose "Searching for SSAS Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "MSAS*:Memory" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $SSAScounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue + MemMB = $_.cookedvalue / 1024 + } } - - Write-Verbose "Searching for SSAS Counters on $Computer" - try - { - $availablecounters = (Get-Counter -ComputerName $Computer -ListSet "MSAS*:Memory" -ErrorAction SilentlyContinue ).paths - (Get-Counter -ComputerName $Computer -Counter $availablecounters -ErrorAction SilentlyContinue ).countersamples | - Where-Object {$_.Path -match $SSAScounters} | - ForEach-Object { - $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] - if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } - [PSCustomObject]@{ - ComputerName = $Computer - SqlInstance = $instance - CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$","")).split(':')[1] - Counter = $_.Path.split("\")[-1] - Pages = $null - MemKB = $_.cookedvalue - MemMB = $_.cookedvalue / 1024 - } - } - } - catch - { + } + catch { Write-Verbose "No SSAS Counters on $Computer" + } + + Write-Verbose "Searching for SSIS Counters on $Computer" + try { + $availablecounters = (Get-Counter -ListSet "*SSIS*" -ErrorAction SilentlyContinue).paths + (Get-Counter -Counter $availablecounters -ErrorAction SilentlyContinue).countersamples | + Where-Object { $_.Path -match $SSIScounters } | + ForEach-Object { + $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] + if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } + [PSCustomObject]@{ + ComputerName = $env:computername + SqlInstance = $instance + CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[1] + Counter = $_.Path.split("\")[-1] + Pages = $null + MemKB = $_.cookedvalue / 1024 + MemMB = $_.cookedvalue / 1024 / 1024 + } } - - Write-Verbose "Searching for SSIS Counters on $Computer" - try - { - $availablecounters = (Get-Counter -ComputerName $Computer -ListSet "*SSIS*" -ErrorAction SilentlyContinue ).paths - (Get-Counter -ComputerName $Computer -Counter $availablecounters -ErrorAction SilentlyContinue ).countersamples | - Where-Object {$_.Path -match $SSIScounters} | - ForEach-Object { - $instance = (($_.Path.split("\")[-2]).replace("mssql`$", "")).split(':')[0] - if ($instance -eq 'sqlserver') { $instance = 'mssqlserver' } - [PSCustomObject]@{ - ComputerName = $Computer - SqlInstance = $instance - CounterInstance = (($_.Path.split("\")[-2]).replace("mssql`$","")).split(':')[1] - Counter = $_.Path.split("\")[-1] - Pages = $null - MemKB = $_.cookedvalue / 1024 - MemMB = $_.cookedvalue / 1024 / 1024 - } - } - } - catch - { + } + catch { Write-Verbose "No SSIS Counters on $Computer" + } + } + } + + process { + foreach ($Computer in $ComputerName.ComputerName) { + $reply = Resolve-DbaNetworkName -ComputerName $computer -Credential $Credential -ErrorAction SilentlyContinue + if ($reply.FullComputerName) { + $Computer = $reply.FullComputerName + try { + Write-Message -Level Verbose -Message "Connecting to $Computer" + Invoke-Command2 -ComputerName $Computer -Credential $Credential -ScriptBlock $scriptblock -argumentlist $Memcounters, $Plancounters, $BufManpagecounters, $SSAScounters, $SSIScounters + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer -Continue } } - else - { - Write-Warning "Can't connect to $Computer." - Continue - } + else { + Write-Message -Level Warning -Message "Can't resolve $Computer." + Continue + } } } - END - {} -} +} \ No newline at end of file diff --git a/functions/Get-DbaMsdtc.ps1 b/functions/Get-DbaMsdtc.ps1 index cb320f9227ed..61cee94db2ed 100644 --- a/functions/Get-DbaMsdtc.ps1 +++ b/functions/Get-DbaMsdtc.ps1 @@ -1,180 +1,136 @@ -Function Get-DbaMsdtc -{ -<# -.SYNOPSIS -Displays information about the Distributed Transactioon Coordinator (MSDTC) on a server - -.DESCRIPTION -Returns a custom object with Computer name, state of the MSDTC Service, security settings of MSDTC and CID's - -Requires: Windows administrator access on Servers - -.PARAMETER ComputerName -The SQL Server (or server in general) that you're connecting to. - -.NOTES -Tags: WSMan, CIM -Author: Klaas Vandenberghe ( powerdbaklaas ) - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaMsdtc - -.EXAMPLE -Get-DbaMsdtc -ComputerName srv0042 - -Get DTC status for the server srv0042 - -ComputerName : srv0042 -DTCServiceName : Distributed Transaction Coordinator -DTCServiceState : Running -DTCServiceStatus : OK -DTCServiceStartMode : Manual -DTCServiceAccount : NT AUTHORITY\NetworkService -DTCCID_MSDTC : b2aefacb-85d1-46b8-8047-7bb88e08d38a -DTCCID_MSDTCUIS : 205bd32c-b022-4d0c-aa3e-2e5dc65c6d35 -DTCCID_MSDTCTIPGW : 3e743aa0-ead6-4569-ba7b-fe1aaea0a1eb -DTCCID_MSDTCXATM : 667fc4b8-c2f5-4c3f-ad75-728b113f36c5 -networkDTCAccess : 0 -networkDTCAccessAdmin : 0 -networkDTCAccessClients : 0 -networkDTCAccessInbound : 0 -networkDTCAccessOutBound : 0 -networkDTCAccessTip : 0 -networkDTCAccessTransactions : 0 -XATransactions : 0 - - -.EXAMPLE -$Computers = (Get-Content D:\configfiles\SQL\MySQLInstances.txt | % {$_.split('\')[0]}) -$Computers | Get-DbaMsdtc - -Get DTC status for all the computers in a .txt file - - -.EXAMPLE -Get-DbaMsdtc -Computername $Computers | where { $_.dtcservicestate -ne 'running' } - -Get DTC status for all the computers where the MSDTC Service is not running - - -.EXAMPLE -Get-DbaMsdtc -ComputerName srv0042 | Out-Gridview - - -Get DTC status for the computer srv0042 and show in a grid view - - - -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline = $true)] - [Alias('cn', 'host', 'Server')] - [string[]]$Computername = $env:COMPUTERNAME - ) - -BEGIN - { - $functionName = (Get-PSCallstack)[0].Command - $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique - $query = "Select * FROM Win32_Service WHERE Name = 'MSDTC'" - $DTCSecurity = { - Get-ItemProperty -Path HKLM:\Software\Microsoft\MSDTC\Security | - Select-Object PSPath, PSComputerName, AccountName, networkDTCAccess, - networkDTCAccessAdmin, networkDTCAccessClients, networkDTCAccessInbound, - networkDTCAccessOutBound, networkDTCAccessTip, networkDTCAccessTransactions, XATransactions - } - $DTCCIDs = { - New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null - Get-ItemProperty -Path HKCR:\CID\*\Description | - Select-Object @{ l = 'Data'; e = { $_.'(default)' } }, @{ l = 'CID'; e = { $_.PSParentPath.split('\')[-1] } } - Remove-PSDrive -Name HKCR | Out-Null - } - } -PROCESS - { - ForEach ($Computer in $Computername) - { - $reg = $CIDs = $null - $CIDHash = @{} - if ( Test-PSRemoting -ComputerName $Computer ) - { - $dtcservice = $null - Write-Verbose "$functionName - Getting DTC on $computer via WSMan" - $dtcservice = Get-Ciminstance -ComputerName $Computer -Query $query - if ( $null -eq $dtcservice ) - { - Write-Warning "$functionName - Can't connect to CIM on $Computer via WSMan" - } - - Write-Verbose "$functionName - Getting MSDTC Security Registry Values on $Computer" - $reg = Invoke-Command -ComputerName $Computer -ScriptBlock $DTCSecurity - if ( $null -eq $reg ) - { - Write-Warning "$functionName - Can't connect to MSDTC Security registry on $Computer" - } - Write-Verbose "$functionName - Getting MSDTC CID Registry Values on $Computer" - $CIDs = Invoke-Command -ComputerName $Computer -ScriptBlock $DTCCIDs - if ( $null -ne $CIDs ) - { - foreach ($key in $CIDs) { $CIDHash.Add($key.Data, $key.CID) } - } - else - { - Write-Warning "$functionName - Can't connect to MSDTC CID registry on $Computer" - } - } - else - { - Write-Verbose "$functionName - PSRemoting is not enabled on $Computer" - try - { - Write-Verbose "$functionName - Failed To get DTC via WinRM. Getting DTC on $computer via DCom" - $SessionParams = @{ } - $SessionParams.ComputerName = $Computer - $SessionParams.SessionOption = (New-CimSessionOption -Protocol Dcom) - $Session = New-CimSession @SessionParams - $dtcservice = Get-Ciminstance -CimSession $Session -Query $query - } - catch - { - Write-Warning "$functionName - Can't connect to CIM on $Computer via DCom" - continue - } +#ValidationTags#Messaging# +function Get-DbaMsdtc { + <# + .SYNOPSIS + Displays information about the Distributed Transaction Coordinator (MSDTC) on a server + + .DESCRIPTION + Returns a custom object with Computer name, state of the MSDTC Service, security settings of MSDTC and CID's + + Requires: Windows administrator access on Servers + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. + + .NOTES + Tags: Msdtc, dtc + Author: Klaas Vandenberghe ( powerdbaklaas ) + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaMsdtc + + .EXAMPLE + Get-DbaMsdtc -ComputerName srv0042 + + Get DTC status for the server srv0042 + + .EXAMPLE + $Computers = (Get-Content D:\configfiles\SQL\MySQLInstances.txt | % {$_.split('\')[0]}) + $Computers | Get-DbaMsdtc + + Get DTC status for all the computers in a .txt file + + .EXAMPLE + Get-DbaMsdtc -Computername $Computers | where { $_.dtcservicestate -ne 'running' } + + Get DTC status for all the computers where the MSDTC Service is not running + + .EXAMPLE + Get-DbaMsdtc -ComputerName srv0042 | Out-Gridview + + Get DTC status for the computer srv0042 and show in a grid view + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias('cn', 'host', 'Server')] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME + ) + + begin { + $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique + $query = "Select * FROM Win32_Service WHERE Name = 'MSDTC'" + $dtcSecurity = { + Get-ItemProperty -Path HKLM:\Software\Microsoft\MSDTC\Security | + Select-Object PSPath, PSComputerName, AccountName, networkDTCAccess, + networkDTCAccessAdmin, networkDTCAccessClients, networkDTCAccessInbound, + networkDTCAccessOutBound, networkDTCAccessTip, networkDTCAccessTransactions, XATransactions } - if ( $dtcservice ) - { - [PSCustomObject]@{ - ComputerName = $dtcservice.PSComputerName - DTCServiceName = $dtcservice.DisplayName - DTCServiceState = $dtcservice.State - DTCServiceStatus = $dtcservice.Status - DTCServiceStartMode = $dtcservice.StartMode - DTCServiceAccount = $dtcservice.StartName - DTCCID_MSDTC = $CIDHash['MSDTC'] - DTCCID_MSDTCUIS = $CIDHash['MSDTCUIS'] - DTCCID_MSDTCTIPGW = $CIDHash['MSDTCTIPGW'] - DTCCID_MSDTCXATM = $CIDHash['MSDTCXATM'] - networkDTCAccess = $reg.networkDTCAccess - networkDTCAccessAdmin = $reg.networkDTCAccessAdmin - networkDTCAccessClients = $reg.networkDTCAccessClients - networkDTCAccessInbound = $reg.networkDTCAccessInbound - networkDTCAccessOutBound = $reg.networkDTCAccessOutBound - networkDTCAccessTip = $reg.networkDTCAccessTip - networkDTCAccessTransactions = $reg.networkDTCAccessTransactions - XATransactions = $reg.XATransactions - } + $dtcCids = { + New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null + Get-ItemProperty -Path HKCR:\CID\*\Description | + Select-Object @{ l = 'Data'; e = { $_.'(default)' } }, @{ l = 'CID'; e = { $_.PSParentPath.split('\')[-1] } } + Remove-PSDrive -Name HKCR | Out-Null + } + } + process { + foreach ($computer in $ComputerName) { + $reg = $cids = $null + $cidHash = @{} + if ( Test-PSRemoting -ComputerName $computer ) { + $dtcservice = $null + Write-Message -Level Verbose -Message "Getting DTC on $computer via WSMan" + $dtcservice = Get-Ciminstance -ComputerName $computer -Query $query + if ( $null -eq $dtcservice ) { + Write-Warning "Can't connect to CIM on $computer via WSMan" + } + + Write-Message -Level Verbose -Message "Getting MSDTC Security Registry Values on $computer" + $reg = Invoke-Command -ComputerName $computer -ScriptBlock $dtcSecurity + if ( $null -eq $reg ) { + Write-Message -Level Warning -Message "Can't connect to MSDTC Security registry on $computer" + } + Write-Message -Level Verbose -Message "Getting MSDTC CID Registry Values on $computer" + $cids = Invoke-Command -ComputerName $computer -ScriptBlock $dtcCids + if ( $null -ne $cids ) { + foreach ($key in $cids) { + $cidHash.Add($key.Data, $key.CID) + } + } + else { + Write-Message -Level Warning -Message "Can't connect to MSDTC CID registry on $computer" + } + } + else { + Write-Message -Level Verbose -Message "PSRemoting is not enabled on $computer" + try { + Write-Message -Level Verbose -Message "Failed To get DTC via WinRM. Getting DTC on $computer via DCom" + $SessionParams = @{ } + $SessionParams.ComputerName = $Computer + $SessionParams.SessionOption = (New-CimSessionOption -Protocol Dcom) + $Session = New-CimSession @SessionParams + $dtcservice = Get-Ciminstance -CimSession $Session -Query $query + } + catch { + Stop-Function -Message "Can't connect to CIM on $computer via DCom" -Target $computer -ErrorRecord $_ -Continue + } + } + if ( $dtcservice ) { + [PSCustomObject]@{ + ComputerName = $dtcservice.PSComputerName + DTCServiceName = $dtcservice.DisplayName + DTCServiceState = $dtcservice.State + DTCServiceStatus = $dtcservice.Status + DTCServiceStartMode = $dtcservice.StartMode + DTCServiceAccount = $dtcservice.StartName + DTCCID_MSDTC = $cidHash['MSDTC'] + DTCCID_MSDTCUIS = $cidHash['MSDTCUIS'] + DTCCID_MSDTCTIPGW = $cidHash['MSDTCTIPGW'] + DTCCID_MSDTCXATM = $cidHash['MSDTCXATM'] + networkDTCAccess = $reg.networkDTCAccess + networkDTCAccessAdmin = $reg.networkDTCAccessAdmin + networkDTCAccessClients = $reg.networkDTCAccessClients + networkDTCAccessInbound = $reg.networkDTCAccessInbound + networkDTCAccessOutBound = $reg.networkDTCAccessOutBound + networkDTCAccessTip = $reg.networkDTCAccessTip + networkDTCAccessTransactions = $reg.networkDTCAccessTransactions + XATransactions = $reg.XATransactions + } + } } } - } } diff --git a/functions/Get-DbaNetworkActivity.ps1 b/functions/Get-DbaNetworkActivity.ps1 new file mode 100644 index 000000000000..7b09d889502f --- /dev/null +++ b/functions/Get-DbaNetworkActivity.ps1 @@ -0,0 +1,90 @@ +function Get-DbaNetworkActivity { + <# + .SYNOPSIS + Gets the Current traffic on every Network Interface on a computer. + + .DESCRIPTION + Gets the Current traffic on every Network Interface on a computer. + See https://msdn.microsoft.com/en-us/library/aa394293(v=vs.85).aspx + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Network + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaNetworkActivity + + .EXAMPLE + Get-DbaNetworkActivity -ComputerName sqlserver2014a + + Gets the Current traffic on every Network Interface on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaNetworkActivity + + Gets the Current traffic on every Network Interface on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaNetworkActivity -ComputerName sql1,sql2 | Out-Gridview + + Gets the Current traffic on every Network Interface on computers sql1 and sql2, and shows them in a grid view. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [string[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique + $sessionoption = New-CimSessionOption -Protocol DCom + } + PROCESS { + foreach ($computer in $ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ( $Server.FullComputerName ) { + $Computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan" + $CIMsession = New-CimSession -ComputerName $Computer -ErrorAction SilentlyContinue -Credential $Credential + if ( -not $CIMSession ) { + Write-Message -Level Verbose -Message "Creating CIMSession on $computer over WSMan failed. Creating CIMSession on $computer over DCom" + $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential + } + if ( $CIMSession ) { + Write-Message -Level Verbose -Message "Getting properties for Network Interfaces on $computer" + $NICs = Get-CimInstance -CimSession $CIMSession -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface + $NICs | Add-Member -Force -MemberType ScriptProperty -Name ComputerName -Value { $computer } + $NICs | Add-Member -Force -MemberType ScriptProperty -Name Bandwith -Value { switch ( $this.CurrentBandWidth ) { 10000000000 { '10Gb' } 1000000000 { '1Gb' } 100000000 { '100Mb' } 10000000 { '10Mb' } 1000000 { '1Mb' } 100000 { '100Kb' } default { 'Low' } } } + foreach ( $NIC in $NICs ) { Select-DefaultView -InputObject $NIC -Property 'ComputerName', 'Name as NIC', 'BytesReceivedPersec', 'BytesSentPersec', 'BytesTotalPersec', 'Bandwidth'} + } #if CIMSession + else { + Write-Message -Level Warning -Message "Can't create CIMSession on $computer" + } + } #if computername + else { + Write-Message -Level Warning -Message "can't connect to $computer" + } + } #foreach computer + } #PROCESS +} #function \ No newline at end of file diff --git a/functions/Get-DbaNetworkCertificate.ps1 b/functions/Get-DbaNetworkCertificate.ps1 new file mode 100644 index 000000000000..e924a4355a81 --- /dev/null +++ b/functions/Get-DbaNetworkCertificate.ps1 @@ -0,0 +1,132 @@ +function Get-DbaNetworkCertificate { + <# +.SYNOPSIS +Simplifies finding computer certificates that are candidates for using with SQL Server's network encryption + +.DESCRIPTION +Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + +.PARAMETER ComputerName +The target SQL Server - defaults to localhost. If target is a cluster, you must specify the distinct nodes. + +.PARAMETER Credential +Allows you to login to $ComputerName using alternative credentials. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaNetworkCertificate +Gets computer certificates on localhost that are candidates for using with SQL Server's network encryption + +.EXAMPLE +Get-DbaNetworkCertificate -ComputerName sql2016 + +Gets computer certificates on sql2016 that are being used for SQL Server network encryption + +#> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($computer in $computername) { + + Write-Message -Level Verbose -Message "Connecting to SQL WMI on $($computer.ComputerName)" + try { + $sqlwmis = Invoke-ManagedComputerCommand -ComputerName $computer.ComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -match "SQL Server \(" + } + catch { + Stop-Function -Message $_ -Target $sqlwmi -Continue + } + + foreach ($sqlwmi in $sqlwmis) { + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Write-Message -Level Warning -Message "Can't find instance $vsname on $env:COMPUTERNAME" + return + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $computer } + + Write-Message -Level Verbose -Message "Regroot: $regroot" + Write-Message -Level Verbose -Message "ServiceAcct: $serviceaccount" + Write-Message -Level Verbose -Message "InstanceName: $instancename" + Write-Message -Level Verbose -Message "VSNAME: $vsname" + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$regroot\MSSQLServer\SuperSocketNetLib" + + $thumbprint = (Get-ItemProperty -Path $regpath -Name Certificate -ErrorAction SilentlyContinue).Certificate + + try { + $cert = Get-ChildItem Cert:\LocalMachine -Recurse -ErrorAction Stop | Where-Object Thumbprint -eq $Thumbprint + } + catch { + # Don't care - sometimes there's errors that are thrown for apparent good reason + } + + if (!$cert) { continue } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + FriendlyName = $cert.FriendlyName + DnsNameList = $cert.DnsNameList + Thumbprint = $cert.Thumbprint + Generated = $cert.NotBefore + Expires = $cert.NotAfter + IssuedTo = $cert.Subject + IssuedBy = $cert.Issuer + Certificate = $cert + } + } + + Write-Message -Level Verbose -Message "Connecting to $computer to get a list of certs" + try { + Invoke-Command2 -ComputerName $computer.ComputerName -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -ExcludeProperty Certificate + } + catch { + Stop-Function -Message $_ -ErrorRecord $_ -Target $ComputerName -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaOpenTransaction.ps1 b/functions/Get-DbaOpenTransaction.ps1 new file mode 100644 index 000000000000..70904a7a0099 --- /dev/null +++ b/functions/Get-DbaOpenTransaction.ps1 @@ -0,0 +1,100 @@ +function Get-DbaOpenTransaction { + <# + .SYNOPSIS + Displays all open transactions. + + .DESCRIPTION + This command is based on open transaction script published by Paul Randal. + Reference: https://www.sqlskills.com/blogs/paul/script-open-transactions-with-text-and-plans/ + + .PARAMETER SqlInstance + The SQL Server instance + + .PARAMETER SqlCredential + Connect using alternative credentials + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Process, Session, ActivityMonitor + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaOpenTransaction + + .EXAMPLE + Get-DbaOpenTransaction -SqlInstance sqlserver2014a + + Returns open transactions for sqlserver2014a + + .EXAMPLE + Get-DbaOpenTransaction -SqlInstance sqlserver2014a -SqlCredential (Get-Credential sqladmin) + + Logs into sqlserver2014a using the login "sqladmin" + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + begin { + $sql = " + SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + [s_tst].[session_id] as Spid, + [s_es].[login_name] as Login, + DB_NAME (s_tdt.database_id) AS [Database], + [s_tdt].[database_transaction_begin_time] AS [BeginTime], + [s_tdt].[database_transaction_log_bytes_used] AS [LogBytesUsed], + [s_tdt].[database_transaction_log_bytes_reserved] AS [LogBytesReserved], + [s_est].text AS [LastQuery], + [s_eqp].[query_plan] AS [LastPlan] + FROM + sys.dm_tran_database_transactions [s_tdt] + JOIN + sys.dm_tran_session_transactions [s_tst] + ON + [s_tst].[transaction_id] = [s_tdt].[transaction_id] + JOIN + sys.[dm_exec_sessions] [s_es] + ON + [s_es].[session_id] = [s_tst].[session_id] + JOIN + sys.dm_exec_connections [s_ec] + ON + [s_ec].[session_id] = [s_tst].[session_id] + LEFT OUTER JOIN + sys.dm_exec_requests [s_er] + ON + [s_er].[session_id] = [s_tst].[session_id] + CROSS APPLY + sys.dm_exec_sql_text ([s_ec].[most_recent_sql_handle]) AS [s_est] + OUTER APPLY + sys.dm_exec_query_plan ([s_er].[plan_handle]) AS [s_eqp] + ORDER BY + [BeginTime] ASC" + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $server.Query($sql) + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaOperatingSystem.ps1 b/functions/Get-DbaOperatingSystem.ps1 new file mode 100644 index 000000000000..20b8911152c7 --- /dev/null +++ b/functions/Get-DbaOperatingSystem.ps1 @@ -0,0 +1,147 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaOperatingSystem { + <# + .SYNOPSIS + Gets operating system information from the server. + + .DESCRIPTION + Gets operating system information from the server and returns as an object. + + .PARAMETER ComputerName + Target computer(s). If no computer name is specified, the local computer is targeted + + .PARAMETER Credential + Alternate credential object to use for accessing the target computer(s). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ServerInfo, OperatingSystem + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https: //dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaOperatingSystem + + .EXAMPLE + Get-DbaOperatingSystem + + Returns information about the local computer's operating system + + .EXAMPLE + Get-DbaOperatingSystem -ComputerName sql2016 + + Returns information about the sql2016's operating system + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($computer in $ComputerName) { + Write-Message -Level Verbose -Message "Attempting to connect to $computer" + $server = Resolve-DbaNetworkName -ComputerName $computer.ComputerName -Credential $Credential + + $computerResolved = $server.FullComputerName + + if (!$computerResolved) { + Write-Message -Level Warning -Message "Unable to resolve hostname of $computer. Skipping." + continue + } + + try { + $psVersion = Invoke-Command2 -ComputerName $computerResolved -Credential $Credential -ScriptBlock { $PSVersionTable.PSVersion } + } + catch { + Stop-Function -Message "Failure collecting PowerShell version on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $os = Get-DbaCmObject -ClassName Win32_OperatingSystem -ComputerName $computerResolved -Credential $Credential -EnableException + } + else { + $os = Get-DbaCmObject -ClassName Win32_OperatingSystem -ComputerName $computerResolved -EnableException + } + } + catch { + Stop-Function -Message "Failure collecting OS information on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $tz = Get-DbaCmObject -ClassName Win32_TimeZone -ComputerName $computerResolved -Credential $Credential -EnableException + } + else { + $tz = Get-DbaCmObject -ClassName Win32_TimeZone -ComputerName $computerResolved -EnableException + } + } + catch { + Stop-Function -Message "Failure collecting TimeZone information on $computer" -Target $computer -ErrorRecord $_ + return + } + + try { + if (Test-Bound "Credential") { + $powerPlan = Get-DbaCmObject -ClassName Win32_PowerPlan -Namespace "root\cimv2\power" -ComputerName $computerResolved -Credential $Credential -EnableException | Select-Object ElementName, InstanceId, IsActive + } + else { + $powerPlan = Get-DbaCmObject -ClassName Win32_PowerPlan -Namespace "root\cimv2\power" -ComputerName $computerResolved -EnableException | Select-Object ElementName, InstanceId, IsActive + } + } + catch { + Stop-Function -Message "Failure collecting PowerPlan information on $computer" -Target $computer -ErrorRecord $_ + return + } + + $activePowerPlan = ($powerPlan | Where-Object IsActive).ElementName -join ',' + $language = Get-Language $os.OSLanguage + + [PSCustomObject]@{ + ComputerName = $computerResolved + Manufacturer = $os.Manufacturer + Organization = $os.Organization + Architecture = $os.OSArchitecture + Version = $os.Version + Build = $os.BuildNumber + Caption = $os.Caption + InstallDate = [DbaDateTime]$os.InstallDate + LastBootTime = [DbaDateTime]$os.LastBootUpTime + LocalDateTime = [DbaDateTime]$os.LocalDateTime + PowerShellVersion = "$($psVersion.Major).$($psVersion.Minor)" + TimeZone = $tz.Caption + TimeZoneStandard = $tz.StandardName + TimeZoneDaylight = $tz.DaylightName + BootDevice = $os.BootDevice + TotalVisibleMemory = [DbaSize]($os.TotalVisibleMemorySize * 1024) + FreePhysicalMemory = [DbaSize]($os.FreePhysicalMemory * 1024) + TotalVirtualMemory = [DbaSize]($os.TotalVirtualMemorySize * 1024) + FreeVirtualMemory = [DbaSize]($os.FreeVirtualMemory * 1024) + ActivePowerPlan = $activePowerPlan + Language = $language.Name + LanguageId = $language.LCID + LanguageKeyboardLayoutId = $language.KeyboardLayoutId + LanguageTwoLetter = $language.TwoLetterISOLanguageName + LanguageThreeLetter = $language.ThreeLetterISOLanguageName + LanguageAlias = $language.DisplayName + LanguageNative = $language.NativeName + CodeSet = $os.CodeSet + CountryCode = $os.CountryCode + Locale = $os.Locale + } | Select-DefaultView -Property ComputerName, Manufacturer, Organization, Architecture, Version, Caption, LastBootTime, LocalDateTime, PowerShellVersion, TimeZone, TotalVisibleMemory, ActivePowerPlan, LanguageNative + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaOrphanUser.ps1 b/functions/Get-DbaOrphanUser.ps1 new file mode 100644 index 000000000000..fee7ed04c659 --- /dev/null +++ b/functions/Get-DbaOrphanUser.ps1 @@ -0,0 +1,131 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaOrphanUser { + <# + .SYNOPSIS + Get orphaned users. + + .DESCRIPTION + An orphan user is defined by a user that does not have their matching login. (Login property = ""). + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Orphan, Database, User, Security, Login + Author: Claudio Silva (@ClaudioESSilva) + Author: Garry Bargsley (@gbargsley) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + .LINK + + https://dbatools.io/Get-DbaOrphanUser + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 + Finds all orphan users without matching Logins in all databases present on server 'localhost\sql2016'. + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 -SqlCredential $cred + Finds all orphan users without matching Logins in all databases present on server 'localhost\sql2016'. SQL Server authentication will be used in connecting to the server. + + .EXAMPLE + Get-DbaOrphanUser -SqlInstance localhost\sql2016 -Database db1 + Finds orphan users without matching Logins in the db1 database present on server 'localhost\sql2016'. + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $SqlInstance." + continue + } + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($DatabaseCollection.Count -gt 0) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + Write-Message -Level Verbose -Message "Validating users on database '$db'." + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false } + + if ($UsersToWork.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found" + foreach ($user in $UsersToWork) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $user.Name + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database '$db'." + } + #reset collection + $UsersToWork = $null + } + catch { + Stop-Function -Message $_ -Continue + } + } + } + else { + Write-Message -Level VeryVerbose -Message "There are no databases to analyse." + } + } + } + +} \ No newline at end of file diff --git a/functions/Get-DbaPageFileSetting.ps1 b/functions/Get-DbaPageFileSetting.ps1 index 251a93df5e8f..a6ac484001fb 100644 --- a/functions/Get-DbaPageFileSetting.ps1 +++ b/functions/Get-DbaPageFileSetting.ps1 @@ -1,155 +1,117 @@ -Function Get-DbaPageFileSetting -{ -<# -.SYNOPSIS -Returns information about the network connection of the target computer including NetBIOS name, IP Address, domain name and fully qualified domain name (FQDN). - -.DESCRIPTION - WMI class Win32_ComputerSystem tells us if Page File is managed automatically. - If TRUE all other properties do not exist. - If FALSE classes Win32_PageFile, Win32_PageFileSetting en Win32_PageFileUsage are examined. - CIM is used, first via WinRM, and if not successful, via DCOM. - This function needs to be executed as a user with local admin rights on the target computer(s). - -.PARAMETER ComputerName -The Server that you're connecting to. -This can be the name of a computer, a SMO object, an IP address or a SQL Instance. - -.PARAMETER Credential -Credential object used to connect to the Computer as a different user - -.NOTES -Tags: CIM -Author: Klaas Vandenberghe ( @PowerDBAKlaas ) - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# -.LINK - https://dbatools.io/Get-DbaPageFileSetting - -.EXAMPLE -Get-DbaPageFileSetting -ComputerName ServerA,ServerB - -Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA and ServerB - -.EXAMPLE -'ServerA' | Get-DbaPageFileSetting - -Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA - -#> [CmdletBinding()] - param ( - [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [Alias("cn", "host", "ServerInstance", "Server", "SqlServer")] - [object]$ComputerName, - [PSCredential] [System.Management.Automation.CredentialAttribute()]$Credential - ) - PROCESS - { - foreach ( $Computer in $ComputerName ) - { - $reply = Resolve-DbaNetworkName -ComputerName $Computer -Credential $Credential -ErrorAction silentlycontinue - - if ( !$reply.ComputerName ) # we can reach $computer - { - Write-Warning "$Computer is not available." - continue - } - - $computer = $reply.ComputerName - Write-Verbose "Getting computer information from $Computer via CIM (WSMan)" - $CompSys = Get-CimInstance -ComputerName $Computer -Query "SELECT * FROM win32_computersystem" -ErrorAction SilentlyContinue - - if ( $CompSys ) # we have computersystem class via WSMan - { - Write-Verbose "Successfully retrieved ComputerSystem information on $Computer via CIM (WSMan)" - if ( $CompSys.PSobject.Properties.Name -contains "automaticmanagedpagefile" ) # pagefile exists on $computer - { - Write-Verbose "Successfully retrieved PageFile information on $Computer via CIM (WSMan)" - if ( $CompSys.automaticmanagedpagefile -eq $False ) # pagefile is not automatically managed, so get settings via WSMan - { - $PF = Get-CimInstance -ComputerName $Computer -Query "SELECT * FROM win32_pagefile" # deprecated ! - $PFU = Get-CimInstance -ComputerName $Computer -Query "SELECT * FROM win32_pagefileUsage" - $PFS = Get-CimInstance -ComputerName $Computer -Query "SELECT * FROM win32_pagefileSetting" - } - } - else # pagefile does not exist on $computer, warn and try next computer - { - Write-Warning "$computer Operating System too old. No Pagefile information available." - continue - } - } - else # we do not get computersystem class via WSMan, try via DCom - { - Write-Verbose "No WSMan connection to $Computer" - Write-Verbose "Getting computer information from $Computer via CIM (DCOM)" - - $sessionoption = New-CimSessionOption -Protocol DCOM - $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -Credential $Credential -ErrorAction SilentlyContinue - $CompSys = Get-CimInstance -CimSession $CIMsession -Query "SELECT * FROM win32_computersystem" -ErrorVariable $MyErr -ErrorAction SilentlyContinue - - if ( $CompSys ) # we have computersystem class via DCom - { - Write-Verbose "Successfully retrieved ComputerSystem information on $Computer via CIM (DCOM)" - if ( $CompSys.PSobject.Properties.Name -contains "automaticmanagedpagefile" ) # pagefile exists on $computer - { - Write-Verbose "Successfully retrieved PageFile information on $Computer via CIM (DCOM)" - if ( $CompSys.automaticmanagedpagefile -eq $False ) # pagefile is not automatically managed, so get settings via DCom CimSession - { - $PF = Get-CimInstance -CimSession $CIMsession -Query "SELECT * FROM win32_pagefile" # deprecated ! - $PFU = Get-CimInstance -CimSession $CIMsession -Query "SELECT * FROM win32_pagefileUsage" - $PFS = Get-CimInstance -CimSession $CIMsession -Query "SELECT * FROM win32_pagefileSetting" - } - } - else # pagefile does not exist on $computer, warn and try next computer - { - Write-Warning "$computer Operating System too old. No Pagefile information available." - continue - } - } - else # we don't get computersystem, not wia WSMan nor via DCom, warn and try next computer - { - Write-Warning "No WSMan nor DCom connection to $Computer. If you're not local admin on $Computer, you need to run this command as a different user." - continue - } - } - if ( $CompSys.automaticmanagedpagefile -eq $False ) # pagefile is not automatic managed, so return settings - { - [PSCustomObject]@{ - ComputerName = $Computer - AutoPageFile = $CompSys.automaticmanagedpagefile - FileName = $PF.name # deprecated ! - Status = $PF.status # deprecated ! - LastModified = $PF.LastModified - LastAccessed = $PF.LastAccessed - AllocatedBaseSize = $PFU.AllocatedBaseSize # in MB, between Initial and Maximum Size - InitialSize = $PFS.InitialSize # in MB - MaximumSize = $PFS.MaximumSize # in MB - PeakUsage = $PFU.peakusage # in MB - CurrentUsage = $PFU.currentusage # in MB - } - } - else # pagefile is automatic managed, so there are no settings - { - [PSCustomObject]@{ - ComputerName = $Computer - AutoPageFile = $CompSys.automaticmanagedpagefile - FileName = $null - Status = $null - LastModified = $null - LastAccessed = $null - AllocatedBaseSize = $null - InitialSize = $null - MaximumSize = $null - PeakUsage = $null - CurrentUsage = $null - } | Select-DefaultView -Property ComputerName, AutoPageFile - } - if ( $CIMsession ) { Remove-CimSession $CIMsession } - } - } +function Get-DbaPageFileSetting { +<# + .SYNOPSIS + Returns information on the pagefile configuration of the target computer. + + .DESCRIPTION + This command uses CIM (or other, related computer management tools) to detect the pagefile configuration of the target compuer(s). + Note that this may require local administrator privileges for the relevant computers. + + .PARAMETER ComputerName + The Server that you're connecting to. + This can be the name of a computer, a SMO object, an IP address, an AD COmputer object, a connection string or a SQL Instance. + + .PARAMETER Credential + Credential object used to connect to the Computer as a different user + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CIM + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaPageFileSetting -ComputerName ServerA,ServerB + + Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA and ServerB + + .EXAMPLE + 'ServerA' | Get-DbaPageFileSetting + + Returns a custom object displaying ComputerName, AutoPageFile, FileName, Status, LastModified, LastAccessed, AllocatedBaseSize, InitialSize, MaximumSize, PeakUsage, CurrentUsage for ServerA + + .LINK + https://dbatools.io/Get-DbaPageFileSetting +#> + + [CmdletBinding()] + param ( + [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias("cn", "host", "ServerInstance", "Server", "SqlServer")] + [DbaInstance]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($computer in $ComputerName) { + Write-Message -Level VeryVerbose -Message "Connecting to $($computer.ComputerName)" -Target $computer + $splatDbaCmObject = @{ + ComputerName = $computer + EnableException = $true + } + if ($Credential) { $splatDbaCmObject["Credential"] = $Credential } + + try { + $compSys = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_computersystem" + if (-not $CompSys.automaticmanagedpagefile) { + $pagefiles = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefile" + $pagefileUsages = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefileUsage" + $pagefileSettings = Get-DbaCmObject @splatDbaCmObject -Query "SELECT * FROM win32_pagefileSetting" + } + } + catch { + Stop-Function -Message "Failed to retrieve information from $($computer.ComputerName)" -ErrorRecord $_ -Target $computer -Continue + } + + if (-not $CompSys.automaticmanagedpagefile) { + foreach ($file in $pagefiles) { + $settings = $pagefileSettings | Where-Object Name -EQ $file.Name + $usage = $pagefileUsages | Where-Object Name -EQ $file.Name + + # pagefile is not automatic managed, so return settings + New-Object Sqlcollaborative.Dbatools.Computer.PageFileSetting -Property @{ + ComputerName = $computer.ComputerName + AutoPageFile = $CompSys.automaticmanagedpagefile + FileName = $file.name + Status = $file.status + SystemManaged = ($settings.InitialSize -eq 0) -and ($settings.MaximumSize -eq 0) + LastModified = $file.LastModified + LastAccessed = $file.LastAccessed + AllocatedBaseSize = $usage.AllocatedBaseSize # in MB, between Initial and Maximum Size + InitialSize = $settings.InitialSize # in MB + MaximumSize = $settings.MaximumSize # in MB + PeakUsage = $usage.peakusage # in MB + CurrentUsage = $usage.currentusage # in MB + } + } + } + else { + # pagefile is automatic managed, so there are no settings + New-Object Sqlcollaborative.Dbatools.Computer.PageFileSetting -Property @{ + ComputerName = $computer + AutoPageFile = $CompSys.automaticmanagedpagefile + FileName = $null + Status = $null + SystemManaged = $null + LastModified = $null + LastAccessed = $null + AllocatedBaseSize = $null + InitialSize = $null + MaximumSize = $null + PeakUsage = $null + CurrentUsage = $null + } + } + } + } } diff --git a/functions/Get-DbaPermission.ps1 b/functions/Get-DbaPermission.ps1 index a9c691b3eb54..0270dd6a7646 100644 --- a/functions/Get-DbaPermission.ps1 +++ b/functions/Get-DbaPermission.ps1 @@ -1,249 +1,252 @@ -Function Get-DbaPermission -{ -<# -.SYNOPSIS -Get a list of Server and Database level permissions - -.DESCRIPTION -Retrieves a list of permissions - -Permissions link principals to securables. -Principals exist on Windows, Instance and Database level. -Securables exist on Instance and Database level. -A permission state can be GRANT, DENY or REVOKE. -The permission type can be SELECT, CONNECT, EXECUTE and more. - -See https://msdn.microsoft.com/en-us/library/ms191291.aspx for more information - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Databases -Return information for only specific databases - -.PARAMETER Exclude -Return information for all but these specific databases - -.PARAMETER Detailed -Shows detailed information - -.PARAMETER IncludeServerLevel -Shows also information on Server Level Permissions - -.PARAMETER NoSystemObjects -Excludes all permissions on system securables - -.NOTES -Author: Klaas Vandenberghe ( @PowerDBAKlaas ) - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . - - -.LINK -https://dbatools.io/Get-DbaPermission - -.EXAMPLE -Get-DbaPermission -SqlServer ServerA\sql987 - -Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable - -.EXAMPLE -Get-DbaPermission -SqlServer ServerA\sql987 -Detailed | Format-Table -AutoSize - -Returns a formatted table displaying Server, Database, permission state, permission type, grantee, granteetype, securable and securabletype - -.EXAMPLE -Get-DbaPermission -SqlServer ServerA\sql987 -NoSystemObjects -IncludeServerLevel - -Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable -in all databases and on the server level, but not on system securables - -.EXAMPLE -Get-DbaPermission -SqlServer sql2016 -Databases master - -Returns a custom object with permissions for the master database - -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [string[]]$SqlServer, - [PsCredential]$Credential, - [switch]$Detailed, - [switch]$IncludeServerLevel, - [switch]$NoSystemObjects - ) - - DynamicParam - { - if ($SqlServer) - { - return Get-ParamSqlDatabases -SqlServer $SqlServer[0] -SqlCredential $Credential - } - } - - BEGIN - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - - if ($NoSystemObjects) - { - $ExcludeSystemObjectssql = "WHERE major_id > 0 " - } - $ServPermsql = @" -SELECT - [Server] = @@SERVERNAME - , [Database] = '' - , [PermState] = state_desc - , [PermissionName] = permission_name - , [SecurableType] = COALESCE(O.type_desc,sp.class_desc) - , [Securable] = CASE WHEN class = 100 THEN @@SERVERNAME - WHEN class = 105 THEN OBJECT_NAME(major_id) - ELSE OBJECT_NAME(major_id) - END - , [Grantee] = SUSER_NAME(grantee_principal_id) - , [GranteeType] = pr.type_desc - -FROM sys.server_permissions sp - JOIN sys.server_principals pr ON pr.principal_id = sp.grantee_principal_id - LEFT OUTER JOIN sys.all_objects o ON o.object_id = sp.major_id - -$ExcludeSystemObjectssql - -; -"@ - - $DBPermsql = @" -SELECT - [Server] = @@SERVERNAME - , [Database] = DB_NAME() - , [PermState] = state_desc - , [PermissionName] = permission_name - , [SecurableType] = COALESCE(O.type_desc,dp.class_desc) - , [Securable] = CASE WHEN class = 0 THEN DB_NAME() - WHEN class = 1 THEN ISNULL(s.name + '.','')+OBJECT_NAME(major_id) - WHEN class = 3 THEN SCHEMA_NAME(major_id) END - , [Grantee] = USER_NAME(grantee_principal_id) - , [GranteeType] = pr.type_desc - -FROM sys.database_permissions dp - JOIN sys.database_principals pr ON pr.principal_id = dp.grantee_principal_id - LEFT OUTER JOIN sys.all_objects o ON o.object_id = dp.major_id - LEFT OUTER JOIN sys.schemas s ON s.schema_id = o.schema_id - -$ExcludeSystemObjectssql - -; -"@ - } - - PROCESS - { - foreach ($servername in $SqlServer) - { - try - { - $server = Connect-SqlServer -SqlServer $servername -SqlCredential $Credential - } - catch - { - if ($SqlServer.count -eq 1) - { - throw $_ - } - else - { - Write-Warning "Can't connect to $servername. Moving on." - Continue - } - } - - if ($server.versionMajor -lt 9) - { - Write-Warning "Get-DbaPermission is only supported on SQL Server 2005 and above. Skipping Instance." - Continue - } - - if ($IncludeServerLevel) - { - Write-Debug "T-SQL: $ServPermsql" - - $resultTable = $null - $resultTable = $server.Databases["master"].ExecuteWithResults($ServPermsql).Tables[0] - foreach ($row in $resultTable) - { - $obj = [PSCustomObject]@{ - Server = $row.Server - Database = "" - PermState = $row.PermState - PermName = $row.PermissionName - Securable = $row.Securable - Grantee = $row.Grantee - SecurableType = $row.SecurableType - GranteeType = $row.GranteeType - } - if ($detailed) { $obj } - else { $obj | Select-Object Server, Database, PermState, PermName, Securable, Grantee } - } - } - - $dbs = $server.Databases - - if ($databases.count -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - - if ($exclude.count -gt 0) - { - $dbs = $dbs | Where-Object { $exclude -notcontains $_.Name } - } - - # $dbs = $dbs | Where-Object {$_.IsAccessible} - - foreach ($db in $dbs) - { - Write-Verbose "Processing $($db.name) on $servername" - - if ($db.IsAccessible -eq $false) - { - Write-Warning "The database $($db.name) is not accessible. Skipping database." - Continue - } - - - Write-Debug "T-SQL: $DBPermsql" - - $resultTable = $null - $resultTable = $db.ExecuteWithResults($DBPermsql).Tables[0] - foreach ($row in $resultTable) - { - $obj = [PSCustomObject]@{ - Server = $row.Server - Database = $row.Database - PermState = $row.PermState - PermName = $row.PermissionName - Securable = $row.Securable - Grantee = $row.Grantee - SecurableType = $row.SecurableType - GranteeType = $row.GranteeType - } - if ($detailed) { $obj } - else { $obj | Select-Object Server, Database, PermState, PermName, Securable, Grantee } - } - } - } - } - END - { } +function Get-DbaPermission { + <# + .SYNOPSIS + Get a list of Server and Database level permissions + + .DESCRIPTION + Retrieves a list of permissions + + Permissions link principals to securables. + Principals exist on Windows, Instance and Database level. + Securables exist on Instance and Database level. + A permission state can be GRANT, DENY or REVOKE. + The permission type can be SELECT, CONNECT, EXECUTE and more. + + See https://msdn.microsoft.com/en-us/library/ms191291.aspx for more information + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies one or more database(s) to exclude from processing. + + .PARAMETER IncludeServerLevel + If this switch is enabled, information about Server Level Permissions will be output. + + .PARAMETER NoSystemObjects + If this switch is enabled, permissions on system securables will be excluded. + + .PARAMETER EnableException + If this switch is enabled exceptions will be thrown to the caller, which will need to perform its own exception processing. Otherwise, the function will try to catch the exception, interpret it and provide a friendly error message. + + .NOTES + Tags: Permissions, Databases + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPermission + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 + + Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable. + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 | Format-Table -AutoSize + + Returns a formatted table displaying Server, Database, permission state, permission type, grantee, granteetype, securable and securabletype. + + .EXAMPLE + Get-DbaPermission -SqlInstance ServerA\sql987 -NoSystemObjects -IncludeServerLevel + + Returns a custom object with Server name, Database name, permission state, permission type, grantee and securable + in all databases and on the server level, but not on system securables. + + .EXAMPLE + Get-DbaPermission -SqlInstance sql2016 -Database master + + Returns a custom object with permissions for the master database. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeServerLevel, + [switch]$NoSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + if ($NoSystemObjects) { + $ExcludeSystemObjectssql = "WHERE major_id > 0 " + } + + $ServPermsql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance + , [Database] = '' + , [PermState] = state_desc + , [PermissionName] = permission_name + , [SecurableType] = COALESCE(o.type_desc,sp.class_desc) + , [Securable] = CASE WHEN class = 100 THEN @@SERVERNAME + WHEN class = 105 THEN OBJECT_NAME(major_id) + ELSE OBJECT_NAME(major_id) + END + , [Grantee] = SUSER_NAME(grantee_principal_id) + , [GranteeType] = pr.type_desc + , [revokeStatement] = 'REVOKE ' + permission_name + ' ' + COALESCE(OBJECT_NAME(major_id),'') + ' FROM [' + SUSER_NAME(grantee_principal_id) + ']' + , [grantStatement] = 'GRANT ' + permission_name + ' ' + COALESCE(OBJECT_NAME(major_id),'') + ' TO [' + SUSER_NAME(grantee_principal_id) + ']' + FROM sys.server_permissions sp + JOIN sys.server_principals pr ON pr.principal_id = sp.grantee_principal_id + LEFT OUTER JOIN sys.all_objects o ON o.object_id = sp.major_id + + $ExcludeSystemObjectssql + + UNION ALL + SELECT SERVERPROPERTY('MachineName') AS ComputerName + , ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName + , SERVERPROPERTY('ServerName') AS SqlInstance + , [database] = '' + , [PermState] = 'GRANT' + , [PermissionName] = pb.[permission_name] + , [SecurableType] = pb.class_desc + , [Securable] = @@SERVERNAME + , [Grantee] = spr.name + , [GranteeType] = spr.type_desc + , [revokestatement] = '' + , [grantstatement] = '' + FROM sys.server_principals AS spr + INNER JOIN sys.fn_builtin_permissions('SERVER') AS pb ON + spr.[name]='bulkadmin' AND pb.[permission_name]='ADMINISTER BULK OPERATIONS' + OR + spr.[name]='dbcreator' AND pb.[permission_name]='CREATE ANY DATABASE' + OR + spr.[name]='diskadmin' AND pb.[permission_name]='ALTER RESOURCES' + OR + spr.[name]='processadmin' AND pb.[permission_name] IN ('ALTER ANY CONNECTION', 'ALTER SERVER STATE') + OR + spr.[name]='sysadmin' AND pb.[permission_name]='CONTROL SERVER' + OR + spr.[name]='securityadmin' AND pb.[permission_name]='ALTER ANY LOGIN' + OR + spr.[name]='serveradmin' AND pb.[permission_name] IN ('ALTER ANY ENDPOINT', 'ALTER RESOURCES','ALTER SERVER STATE', 'ALTER SETTINGS','SHUTDOWN', 'VIEW SERVER STATE') + OR + spr.[name]='setupadmin' AND pb.[permission_name]='ALTER ANY LINKED SERVER' + WHERE spr.[type]='R' + ;" + + $DBPermsql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance + , [Database] = DB_NAME() + , [PermState] = state_desc + , [PermissionName] = permission_name + , [SecurableType] = COALESCE(o.type_desc,dp.class_desc) + , [Securable] = CASE WHEN class = 0 THEN DB_NAME() + WHEN class = 1 THEN ISNULL(s.name + '.','')+OBJECT_NAME(major_id) + WHEN class = 3 THEN SCHEMA_NAME(major_id) + WHEN class = 6 THEN SCHEMA_NAME(t.schema_id)+'.' + t.name + END + , [Grantee] = USER_NAME(grantee_principal_id) + , [GranteeType] = pr.type_desc + , [revokeStatement] = 'REVOKE ' + permission_name + ' ON ' + isnull(schema_name(o.object_id)+'.','')+OBJECT_NAME(major_id)+ ' FROM [' + USER_NAME(grantee_principal_id) + ']' + , [grantStatement] = 'GRANT ' + permission_name + ' ON ' + isnull(schema_name(o.object_id)+'.','')+OBJECT_NAME(major_id)+ ' TO [' + USER_NAME(grantee_principal_id) + ']' + FROM sys.database_permissions dp + JOIN sys.database_principals pr ON pr.principal_id = dp.grantee_principal_id + LEFT OUTER JOIN sys.all_objects o ON o.object_id = dp.major_id + LEFT OUTER JOIN sys.schemas s ON s.schema_id = o.schema_id + LEFT OUTER JOIN sys.types t on t.user_type_id = dp.major_id + + $ExcludeSystemObjectssql + + UNION ALL + SELECT SERVERPROPERTY('MachineName') AS ComputerName + , ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName + , SERVERPROPERTY('ServerName') AS SqlInstance + , [database] = DB_NAME() + , [PermState] = '' + , [PermissionName] = p.[permission_name] + , [SecurableType] = p.class_desc + , [Securable] = DB_NAME() + , [Grantee] = dp.name + , [GranteeType] = dp.type_desc + , [revokestatement] = '' + , [grantstatement] = '' + FROM sys.database_principals AS dp + INNER JOIN sys.fn_builtin_permissions('DATABASE') AS p ON + dp.[name]='db_accessadmin' AND p.[permission_name] IN ('ALTER ANY USER', 'CREATE SCHEMA') + OR + dp.[name]='db_backupoperator' AND p.[permission_name] IN ('BACKUP DATABASE', 'BACKUP LOG', 'CHECKPOINT') + OR + dp.[name] IN ('db_datareader', 'db_denydatareader') AND p.[permission_name]='SELECT' + OR + dp.[name] IN ('db_datawriter', 'db_denydatawriter') AND p.[permission_name] IN ('INSERT', 'DELETE', 'UPDATE') + OR + dp.[name]='db_ddladmin' AND + p.[permission_name] IN ('ALTER ANY ASSEMBLY', 'ALTER ANY ASYMMETRIC KEY', + 'ALTER ANY CERTIFICATE', 'ALTER ANY CONTRACT', + 'ALTER ANY DATABASE DDL TRIGGER', 'ALTER ANY DATABASE EVENT', + 'NOTIFICATION', 'ALTER ANY DATASPACE', 'ALTER ANY FULLTEXT CATALOG', + 'ALTER ANY MESSAGE TYPE', 'ALTER ANY REMOTE SERVICE BINDING', + 'ALTER ANY ROUTE', 'ALTER ANY SCHEMA', 'ALTER ANY SERVICE', + 'ALTER ANY SYMMETRIC KEY', 'CHECKPOINT', 'CREATE AGGREGATE', + 'CREATE DEFAULT', 'CREATE FUNCTION', 'CREATE PROCEDURE', + 'CREATE QUEUE', 'CREATE RULE', 'CREATE SYNONYM', 'CREATE TABLE', + 'CREATE TYPE', 'CREATE VIEW', 'CREATE XML SCHEMA COLLECTION', + 'REFERENCES') + OR + dp.[name]='db_owner' AND p.[permission_name]='CONTROL' + OR + dp.[name]='db_securityadmin' AND p.[permission_name] IN ('ALTER ANY APPLICATION ROLE', 'ALTER ANY ROLE', 'CREATE SCHEMA', 'VIEW DEFINITION') + + WHERE dp.[type]='R' + AND dp.is_fixed_role=1 + ;" + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance." + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($IncludeServerLevel) { + Write-Message -Level Debug -Message "T-SQL: $ServPermsql" + $server.Query($ServPermsql) + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance." + + if ($db.IsAccessible -eq $false) { + Write-Warning "The database $db is not accessible. Skipping database." + Continue + } + + Write-Message -Level Debug -Message "T-SQL: $DBPermsql" + $db.ExecuteWithResults($DBPermsql).Tables.Rows + } + } + } } diff --git a/functions/Get-DbaPfAvailableCounter.ps1 b/functions/Get-DbaPfAvailableCounter.ps1 new file mode 100644 index 000000000000..00d688bcabaf --- /dev/null +++ b/functions/Get-DbaPfAvailableCounter.ps1 @@ -0,0 +1,105 @@ +function Get-DbaPfAvailableCounter { + <# + .SYNOPSIS + Gathers list of all available counters on local or remote machines. + + .DESCRIPTION + Gathers list of all available counters on local or remote machines. Note, if you pass a credential object, it will be included in the output for easy reuse in your next piped command. + + Thanks to Daniel Streefkerk for this super fast way of counters + https://daniel.streefkerkonline.com/2016/02/18/use-powershell-to-list-all-windows-performance-counters-and-their-numeric-ids + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Pattern + Specify a pattern for filtering. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfAvailableCounter + + .EXAMPLE + Get-DbaPfAvailableCounter + + Gets all available counters on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* + + Gets all counters matching sql on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -ComputerName sql2017 -Pattern *sql* + + Gets all counters matching sql on the remote server sql2017. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* + + Gets all counters matching sql on the local machine. + + .EXAMPLE + Get-DbaPfAvailableCounter -Pattern *sql* | Add-DbaPfDataCollectorCounter -CollectorSet 'Test Collector Set' -Collector DataCollector01 + + Adds all counters matching "sql" to the DataCollector01 within the 'Test Collector Set' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:ComputerName, + [PSCredential]$Credential, + [string]$Pattern, + [switch]$EnableException + ) + begin { + $scriptblock = { + $counters = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009' -Name 'counter' | Select-Object -ExpandProperty Counter | + Where-Object { $_ -notmatch '[0-90000]' } | Sort-Object | Get-Unique + + foreach ($counter in $counters) { + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Name = $counter + Credential = $args + } + } + } + + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($computer in $ComputerName) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + + try { + if ($pattern) { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $credential -ErrorAction Stop | + Where-Object Name -match $pattern | Select-DefaultView -ExcludeProperty Credential + } + else { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ArgumentList $credential -ErrorAction Stop | + Select-DefaultView -ExcludeProperty Credential + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPfDataCollector.ps1 b/functions/Get-DbaPfDataCollector.ps1 new file mode 100644 index 000000000000..af727ca56c8a --- /dev/null +++ b/functions/Get-DbaPfDataCollector.ps1 @@ -0,0 +1,137 @@ +function Get-DbaPfDataCollector { + <# + .SYNOPSIS + Gets Performance Monitor Data Collectors. + + .DESCRIPTION + Gets Performance Monitor Data Collectors. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollector + + .EXAMPLE + Get-DbaPfDataCollector + + Gets all Collectors on localhost. + + .EXAMPLE + Get-DbaPfDataCollector -ComputerName sql2017 + + Gets all Collectors on sql2017. + + .EXAMPLE + Get-DbaPfDataCollector -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets all Collectors for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Get-DbaPfDataCollector + + Gets all Collectors for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $columns = 'ComputerName', 'DataCollectorSet', 'Name', 'DataCollectorType', 'DataSourceName', 'FileName', 'FileNameFormat', 'FileNameFormatPattern', 'LatestOutputLocation', 'LogAppend', 'LogCircular', 'LogFileFormat', 'LogOverwrite', 'SampleInterval', 'SegmentMaxRecords', 'Counters' + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorSet -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorSetObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorSet." + return + } + } + + foreach ($set in $InputObject) { + $collectorxml = ([xml]$set.Xml).DataCollectorSet.PerformanceCounterDataCollector + foreach ($col in $collectorxml) { + if ($Collector -and $Collector -notcontains $col.Name) { + continue + } + + $outputlocation = $col.LatestOutputLocation + if ($outputlocation) { + $dir = ($outputlocation).Replace(':', '$') + $remote = "\\$($set.ComputerName)\$dir" + } + else { + $remote = $null + } + + [pscustomobject]@{ + ComputerName = $set.ComputerName + DataCollectorSet = $set.Name + Name = $col.Name + FileName = $col.FileName + DataCollectorType = $col.DataCollectorType + FileNameFormat = $col.FileNameFormat + FileNameFormatPattern = $col.FileNameFormatPattern + LogAppend = $col.LogAppend + LogCircular = $col.LogCircular + LogOverwrite = $col.LogOverwrite + LatestOutputLocation = $col.LatestOutputLocation + DataCollectorSetXml = $set.Xml + RemoteLatestOutputLocation = $remote + DataSourceName = $col.DataSourceName + SampleInterval = $col.SampleInterval + SegmentMaxRecords = $col.SegmentMaxRecords + LogFileFormat = $col.LogFileFormat + Counters = $col.Counter + CounterDisplayNames = $col.CounterDisplayName + CollectorXml = $col + DataCollectorObject = $true + Credential = $Credential + } | Select-DefaultView -Property $columns + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPfDataCollectorCounter.ps1 b/functions/Get-DbaPfDataCollectorCounter.ps1 new file mode 100644 index 000000000000..287a65b15cc6 --- /dev/null +++ b/functions/Get-DbaPfDataCollectorCounter.ps1 @@ -0,0 +1,119 @@ +function Get-DbaPfDataCollectorCounter { + <# + .SYNOPSIS + Gets Performance Counters. + + .DESCRIPTION + Gets Performance Counters. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER Counter + The Counter name to capture. This must be in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorCounter + + .EXAMPLE + Get-DbaPfDataCollectorCounter + + Gets all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017 + + Gets all counters for all Collector Sets on on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017 -Counter '\Processor(_Total)\% Processor Time' + + Gets the '\Processor(_Total)\% Processor Time' counter on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets all counters for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Get-DbaPfDataCollector | Get-DbaPfDataCollectorCounter + + Gets all counters for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [string[]]$Counter, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $columns = 'ComputerName', 'Name', 'DataCollectorSet', 'Counters', 'DataCollectorType', 'DataSourceName', 'FileName', 'FileNameFormat', 'FileNameFormatPattern', 'LatestOutputLocation', 'LogAppend', 'LogCircular', 'LogFileFormat', 'LogOverwrite', 'SampleInterval', 'SegmentMaxRecords' + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollector -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollector." + return + } + } + + foreach ($counterobject in $InputObject) { + foreach ($countername in $counterobject.Counters) { + if ($Counter -and $Counter -notcontains $countername) { continue } + [pscustomobject]@{ + ComputerName = $counterobject.ComputerName + DataCollectorSet = $counterobject.DataCollectorSet + DataCollector = $counterobject.Name + DataCollectorSetXml = $counterobject.DataCollectorSetXml + Name = $countername + FileName = $counterobject.FileName + CounterObject = $true + Credential = $Credential + } | Select-DefaultView -ExcludeProperty DataCollectorObject, Credential, CounterObject, DataCollectorSetXml + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPfDataCollectorCounterSample.ps1 b/functions/Get-DbaPfDataCollectorCounterSample.ps1 new file mode 100644 index 000000000000..0a46f8dbd469 --- /dev/null +++ b/functions/Get-DbaPfDataCollectorCounterSample.ps1 @@ -0,0 +1,197 @@ +function Get-DbaPfDataCollectorCounterSample { + <# + .SYNOPSIS + Gets Performance Counter Samples. + + .DESCRIPTION + Gets Performance Counter Samples. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector Set name. + + .PARAMETER Collector + The Collector name. + + .PARAMETER Counter + The Counter name. This must be in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER Continuous + If this switch is enabled, samples will be retrieved continuously until you press CTRL+C. By default, this command gets only one counter sample. You can use the SampleInterval parameter to set the interval for continuous sampling. + + .PARAMETER ListSet + Gets the specified performance counter sets on the computers. Enter the names of the counter sets. Wildcards are permitted. + + .PARAMETER MaxSamples + Specifies the number of samples to get from each counter. The default is 1 sample. To get samples continuously (no maximum sample size), use the Continuous parameter. + + To collect a very large data set, consider running a Get-DbaPfDataCollectorCounterSample command as a Windows PowerShell background job. + + .PARAMETER SampleInterval + Specifies the time between samples in seconds. The minimum value and the default value are 1 second. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorCounter via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorCounterSample + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample + + Gets a single sample for all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -Counter '\Processor(_Total)\% Processor Time' + + Gets a single sample for all counters for all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorCounter -ComputerName sql2017, sql2016 | Out-GridView -PassThru | Get-DbaPfDataCollectorCounterSample -MaxSamples 10 + + Gets 10 samples for all counters for all Collector Sets for servers sql2016 and sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -ComputerName sql2017 + + Gets a single sample for all counters for all Collector Sets on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets a single sample for all counters for the 'System Correlation' CollectorSet on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorCounterSample -CollectorSet 'System Correlation' + + Gets a single sample for all counters for the 'System Correlation' CollectorSet. + #> + [CmdletBinding()] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [string[]]$Counter, + [switch]$Continuous, + [switch[]]$ListSet, + [int]$MaxSamples, + [int]$SampleInterval, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if ($InputObject.Counter -and (Test-Bound -ParameterName Counter -Not)) { + $Counter = $InputObject.Counter + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorCounter -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector + } + } + + if ($InputObject) { + if (-not $InputObject.CounterObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorCounter." + return + } + } + + foreach ($counterobject in $InputObject) { + if ((Test-Bound -ParameterName Counter) -and ($Counter -notcontains $counterobject.Name)) { continue } + $params = @{ + Counter = $counterobject.Name + } + + if (-not ([dbainstance]$counterobject.ComputerName).IsLocalHost) { + $params.Add("ComputerName", $counterobject.ComputerName) + } + + if ($Credential) { + $params.Add("Credential", $Credential) + } + + if ($Continuous) { + $params.Add("Continuous", $Continuous) + } + + if ($ListSet) { + $params.Add("ListSet", $ListSet) + } + + if ($MaxSamples) { + $params.Add("MaxSamples", $MaxSamples) + } + + if ($SampleInterval) { + $params.Add("SampleInterval", $SampleInterval) + } + + if ($Continuous) { + Get-Counter @params + } + else { + try { + $pscounters = Get-Counter @params -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure for $($counterobject.Name) on $($counterobject.ComputerName)." -ErrorRecord $_ -Continue + } + + foreach ($pscounter in $pscounters) { + foreach ($sample in $pscounter.CounterSamples) { + [pscustomobject]@{ + ComputerName = $counterobject.ComputerName + DataCollectorSet = $counterobject.DataCollectorSet + DataCollector = $counterobject.DataCollector + Name = $counterobject.Name + Timestamp = $pscounter.Timestamp + Path = $sample.Path + InstanceName = $sample.InstanceName + CookedValue = $sample.CookedValue + RawValue = $sample.RawValue + SecondValue = $sample.SecondValue + MultipleCount = $sample.MultipleCount + CounterType = $sample.CounterType + SampleTimestamp = $sample.Timestamp + SampleTimestamp100NSec = $sample.Timestamp100NSec + Status = $sample.Status + DefaultScale = $sample.DefaultScale + TimeBase = $sample.TimeBase + Sample = $pscounter.CounterSamples + CounterSampleObject = $true + } | Select-DefaultView -ExcludeProperty Sample, CounterSampleObject + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPfDataCollectorSet.ps1 b/functions/Get-DbaPfDataCollectorSet.ps1 new file mode 100644 index 000000000000..1c37f40bfafd --- /dev/null +++ b/functions/Get-DbaPfDataCollectorSet.ps1 @@ -0,0 +1,190 @@ +function Get-DbaPfDataCollectorSet { + <# + .SYNOPSIS + Gets Performance Monitor Data Collector Set. + + .DESCRIPTION + Gets Performance Monitor Data Collector Set. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER CollectorSet + The Collector set name. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorSet + + .EXAMPLE + Get-DbaPfDataCollectorSet + + Gets all Collector Sets on localhost. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2017 + + Gets all Collector Sets on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2017 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Gets the 'System Correlation' CollectorSet on sql2017 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet | Select * + + Displays extra columns and also exposes the original COM object in DataCollectorSetObject. + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [switch]$EnableException + ) + + begin { + $setscript = { + # Get names / status info + $schedule = New-Object -ComObject "Schedule.Service" + $schedule.Connect() + $folder = $schedule.GetFolder("Microsoft\Windows\PLA") + $tasks = @() + $tasknumber = 0 + $done = $false + do { + try { + $task = $folder.GetTasks($tasknumber) + $tasknumber++ + if ($task) { + $tasks += $task + } + } + catch { + $done = $true + } + } + while ($done -eq $false) + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) + + if ($args[0]) { + $tasks = $tasks | Where-Object Name -in $args[0] + } + + $sets = New-Object -ComObject Pla.DataCollectorSet + foreach ($task in $tasks) { + $setname = $task.Name + switch ($task.State) { + 0 { $state = "Unknown" } + 1 { $state = "Disabled" } + 2 { $state = "Queued" } + 3 { $state = "Ready" } + 4 { $state = "Running" } + } + + try { + # Query changes $sets so work from there + $sets.Query($setname, $null) + $set = $sets.PSObject.Copy() + + $outputlocation = $set.OutputLocation + $latestoutputlocation = $set.LatestOutputLocation + + if ($outputlocation) { + $dir = (Split-Path $outputlocation).Replace(':', '$') + $remote = "\\$env:COMPUTERNAME\$dir" + } + else { + $remote = $null + } + + if ($latestoutputlocation) { + $dir = ($latestoutputlocation).Replace(':', '$') + $remotelatest = "\\$env:COMPUTERNAME\$dir" + } + else { + $remote = $null + } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Name = $setname + LatestOutputLocation = $set.LatestOutputLocation + OutputLocation = $set.OutputLocation + RemoteOutputLocation = $remote + RemoteLatestOutputLocation = $remotelatest + RootPath = $set.RootPath + Duration = $set.Duration + Description = $set.Description + DescriptionUnresolved = $set.DescriptionUnresolved + DisplayName = $set.DisplayName + DisplayNameUnresolved = $set.DisplayNameUnresolved + Keywords = $set.Keywords + Segment = $set.Segment + SegmentMaxDuration = $set.SegmentMaxDuration + SegmentMaxSize = $set.SegmentMaxSize + SerialNumber = $set.SerialNumber + Server = $set.Server + Status = $set.Status + Subdirectory = $set.Subdirectory + SubdirectoryFormat = $set.SubdirectoryFormat + SubdirectoryFormatPattern = $set.SubdirectoryFormatPattern + Task = $set.Task + TaskRunAsSelf = $set.TaskRunAsSelf + TaskArguments = $set.TaskArguments + TaskUserTextArguments = $set.TaskUserTextArguments + Schedules = $set.Schedules + SchedulesEnabled = $set.SchedulesEnabled + UserAccount = $set.UserAccount + Xml = $set.Xml + Security = $set.Security + StopOnCompletion = $set.StopOnCompletion + State = $state.Trim() + DataCollectorSetObject = $true + TaskObject = $task + Credential = $args[1] + } + } + catch { + Write-Warning -Message "Issue with getting Collector Set $setname on $env:Computername : $_." + continue + } + } + } + + $columns = 'ComputerName', 'Name', 'DisplayName', 'Description', 'State', 'Duration', 'OutputLocation', 'LatestOutputLocation', + 'RootPath', 'SchedulesEnabled', 'Segment', 'SegmentMaxDuration', 'SegmentMaxSize', + 'SerialNumber', 'Server', 'StopOnCompletion', 'Subdirectory', 'SubdirectoryFormat', + 'SubdirectoryFormatPattern', 'Task', 'TaskArguments', 'TaskRunAsSelf', 'TaskUserTextArguments', 'UserAccount' + } + process { + foreach ($computer in $ComputerName.ComputerName) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $CollectorSet, $Credential -ErrorAction Stop | Select-DefaultView -Property $columns + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPfDataCollectorSetTemplate.ps1 b/functions/Get-DbaPfDataCollectorSetTemplate.ps1 new file mode 100644 index 000000000000..06e59fb11336 --- /dev/null +++ b/functions/Get-DbaPfDataCollectorSetTemplate.ps1 @@ -0,0 +1,106 @@ +function Get-DbaPfDataCollectorSetTemplate { + <# + .SYNOPSIS + Parses Perf Monitor templates. Defaults to parsing templates in the dbatools template repository (\bin\perfmontemplates\). + + .DESCRIPTION + Parses Perf Monitor XML templates. Defaults to parsing templates in the dbatools template repository (\bin\perfmontemplates\). + + .PARAMETER Path + The path to the template directory. Defaults to the dbatools template repository (\bin\perfmontemplates\). + + .PARAMETER Pattern + Specify a pattern for filtering. Alternatively, you can use Out-GridView -Passthru to select objects and pipe them to Import-DbaPfDataCollectorSetTemplate. + + .PARAMETER Template + Specifies one or more of the templates provided by dbatools. Press tab to cycle through the list to the options. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPfDataCollectorSetTemplate + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate + + Returns information about all the templates in the local dbatools repository. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Out-GridView -PassThru | Import-DbaPfDataCollectorSetTemplate -ComputerName sql2017 | Start-DbaPfDataCollectorSet + + Allows you to select a template, then deploys it to sql2017 and immediately starts the DataCollectorSet. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Select-Object * + + Returns more information about the template, including the full path/filename. + #> + [CmdletBinding()] + param ( + [string[]]$Path = "$script:PSModuleRoot\bin\perfmontemplates\collectorsets", + [string]$Pattern, + [string[]]$Template, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\perfmontemplates\collectorsets.xml" + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($directory in $Path) { + $files = Get-ChildItem "$directory\*.xml" + + if ($Template) { + $files = $files | Where-Object BaseName -in $Template + } + + foreach ($file in $files) { + try { + $xml = [xml](Get-Content $file) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + foreach ($dataset in $xml.DataCollectorSet) { + $meta = $metadata | Where-Object Name -eq $dataset.name + if ($Pattern) { + if ( + ($dataset.Name -match $Pattern) -or + ($dataset.Description -match $Pattern) + ) { + [pscustomobject]@{ + Name = $dataset.name + Source = $meta.Source + UserAccount = $dataset.useraccount + Description = $dataset.Description + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, Path + } + } + else { + [pscustomobject]@{ + Name = $dataset.name + Source = $meta.Source + UserAccount = $dataset.useraccount + Description = $dataset.Description + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, Path + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaPolicy.ps1 b/functions/Get-DbaPolicy.ps1 new file mode 100644 index 000000000000..378517319398 --- /dev/null +++ b/functions/Get-DbaPolicy.ps1 @@ -0,0 +1,113 @@ +function Get-DbaPolicy { + <# + .SYNOPSIS + Returns polices from policy based management from an instance. + + .DESCRIPTION + Returns details of policies with the option to filter on Category and SystemObjects. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Policy + Filters results to only show specific policy + + .PARAMETER Category + Filters results to only show policies in the category selected + + .PARAMETER IncludeSystemObject + By default system objects are filtered out. Use this parameter to INCLUDE them . + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Stephen Bennett (https://sqlnotesfromtheunderground.wordpress.com/) + Tags: Policy, PoilcyBasedManagement + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaPolicy + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 + + Returns all policies from sql2016 server + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 -SqlCredential $cred + + Uses a credential $cred to connect and return all policies from sql2016 instance + + .EXAMPLE + Get-DbaPolicy -SqlInstance sql2016 -Category MorningCheck + + Returns all policies from sql2016 server that part of the PolicyCategory MorningCheck +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [string[]]$Policy, + [string[]]$Category, + [switch]$IncludeSystemObject, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $sqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $server.ConnectionContext.SqlConnectionObject + # DMF is the Declarative Management Framework, Policy Based Management's old name + $store = New-Object Microsoft.SqlServer.Management.DMF.PolicyStore $sqlStoreConnection + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server -Continue + } + + $allpolicies = $store.Policies + + if (-not $IncludeSystemObject) { + $allpolicies = $allpolicies | Where-Object { $_.IsSystemObject -eq 0 } + } + + if ($Category) { + $allpolicies = $allpolicies | Where-Object { $_.PolicyCategory -in $Category } + } + + if ($Policy) { + $allpolicies = $allpolicies | Where-Object { $_.Name -in $Policy } + } + + foreach ($currentpolicy in $allpolicies) { + Write-Message -Level Verbose -Message "Processing $currentpolicy" + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty ComputerName -value $server.NetName + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $currentpolicy -MemberType NoteProperty SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $currentpolicy -ExcludeProperty HelpText, HelpLink, Urn, Properties, Metadata, Parent, IdentityKey, HasScript, PolicyEvaluationStarted, ConnectionProcessingStarted, TargetProcessed, ConnectionProcessingFinished, PolicyEvaluationFinished, PropertyMetadataChanged, PropertyChanged + } + } + } +} diff --git a/functions/Get-DbaPrivilege.ps1 b/functions/Get-DbaPrivilege.ps1 index e21c141f37ff..0f7650054162 100644 --- a/functions/Get-DbaPrivilege.ps1 +++ b/functions/Get-DbaPrivilege.ps1 @@ -1,8 +1,7 @@ -function Get-DbaPrivilege -{ - <# +function Get-DbaPrivilege { + <# .SYNOPSIS - Gets the users with local privileges on one or more computersr. + Gets the users with local privileges on one or more computers. .DESCRIPTION Gets the users with local privileges 'Lock Pages in Memory', 'Instant File Initialization', 'Logon as Batch' on one or more computers. @@ -15,16 +14,19 @@ function Get-DbaPrivilege .PARAMETER Credential Credential object used to connect to the computer as a different user. + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + .NOTES Author: Klaas Vandenberghe ( @PowerDBAKlaas ) Tags: Privilege - dbatools PowerShell module (https://dbatools.io) - Copyright (C) 2016 Chrissy LeMaire - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT - .LINK + .LINK https://dbatools.io/Get-DbaPrivilege .EXAMPLE @@ -32,7 +34,7 @@ function Get-DbaPrivilege Gets the local privileges on computer sqlserver2014a. - .EXAMPLE + .EXAMPLE 'sql1','sql2','sql3' | Get-DbaPrivilege Gets the local privileges on computers sql1, sql2 and sql3. @@ -43,80 +45,94 @@ function Get-DbaPrivilege Gets the local privileges on computers sql1 and sql2, and shows them in a grid view. #> - [CmdletBinding()] - Param ( - [parameter(ValueFromPipeline)] - [Alias("cn","host","Server")] - [string[]]$ComputerName = $env:COMPUTERNAME, - [PSCredential] [System.Management.Automation.CredentialAttribute()]$Credential - ) - -BEGIN - { - function Convert-SIDToUserName ([string] $SID ) { - $objSID = New-Object System.Security.Principal.SecurityIdentifier ("$SID") - $objUser = $objSID.Translate( [System.Security.Principal.NTAccount]) - $objUser.Value + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [dbainstanceparameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + $ResolveSID = @" + function Convert-SIDToUserName ([string] `$SID ) { + `$objSID = New-Object System.Security.Principal.SecurityIdentifier (`"`$SID`") + `$objUser = `$objSID.Translate( [System.Security.Principal.NTAccount]) + `$objUser.Value } - $FunctionName = (Get-PSCallstack)[0].Command - $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique - } -PROCESS - { - foreach ($computer in $ComputerName) - { - Write-Verbose "$FunctionName - Connecting to $computer" - if ( Test-PSRemoting -ComputerName $Computer ) - { - Write-Verbose "$FunctionName - Getting Privileges on $Computer" - $Priv = $null - $Priv = Invoke-Command -ComputerName $computer -ScriptBlock {$temp = ([System.IO.Path]::GetTempPath()).TrimEnd("") ; secedit /export /cfg $temp\secpol.cfg > $NULL ; - Get-Content $temp\secpol.cfg | Where-Object { $_ -match "SeBatchLogonRight" -or $_ -match 'SeManageVolumePrivilege' -or $_ -match 'SeLockMemoryPrivilege' }} - try - { - Write-Verbose "$FunctionName - Getting Batch Logon Privileges on $Computer" - $BL = ($Priv | Where-Object {$_ -match "SeBatchLogonRight"}).substring(20).split(",").replace("`*","") | ForEach-Object { Convert-SIDToUserName -SID $_ } - } - catch - { - Write-Verbose "$FunctionName - No users with Batch Logon Rights on $computer" - } - try - { - Write-Verbose "$FunctionName - Getting Instant File Initialization Privileges on $Computer" - $IFI = ($Priv | Where-Object {$_ -like 'SeManageVolumePrivilege*'}).substring(26).split(",").replace("`*","") | ForEach-Object { Convert-SIDToUserName -SID $_ } - } - catch - { - Write-Verbose "$FunctionName - No users with Instant File Initialization Rights on $computer" - } - try - { - Write-Verbose "$FunctionName - Getting Lock Pages in Memory Privileges on $Computer" - $LPIM = ($Priv | Where-Object {$_ -like 'SeLockMemoryPrivilege*'}).substring(24).split(",").replace("`*","") | ForEach-Object { Convert-SIDToUserName -SID $_ } - } - catch - { - Write-Verbose "$FunctionName - No users with Lock Pages in Memory Rights on $computer" - } - $users = $BL + $IFI + $LPIM | Select-Object -Unique - $users | ForEach-Object { - [PSCustomObject]@{ - ComputerName = $computer - User = $_ - LogonAsBatchPrivilege = $BL -contains $_ - InstantFileInitializationPrivilege = $IFI -contains $_ - LockPagesInMemoryPrivilege = $LPIM -contains $_ - } - } - Write-Verbose "$FunctionName - Removing secpol file on $computer" - Invoke-Command -ComputerName $computer -ScriptBlock {$temp = ([System.IO.Path]::GetTempPath()).TrimEnd("") ; Remove-Item $temp\secpol.cfg -Force > $NULL } - } - else - { - Write-Warning "$FunctionName - Failed to connect to $Computer" - } +"@ + $ComputerName = $ComputerName.ComputerName | Select-Object -Unique + } + process { + foreach ($computer in $ComputerName) { + try { + Write-Message -Level Verbose -Message "Connecting to $computer" + if (Test-PSRemoting -ComputerName $Computer) { + Write-Message -Level Verbose -Message "Getting Privileges on $Computer" + $Priv = $null + $Priv = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); secedit /export /cfg $temp\secpolByDbatools.cfg > $NULL; + Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -match "SeBatchLogonRight" -or $_ -match 'SeManageVolumePrivilege' -or $_ -match 'SeLockMemoryPrivilege' } + } + + Write-Message -Level Verbose -Message "Getting Batch Logon Privileges on $Computer" + $BL = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -match "SeBatchLogonRight" }).substring(20).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + if ($BL.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Batch Logon Rights on $computer" + } + Write-Message -Level Verbose -Message "Getting Instant File Initialization Privileges on $Computer" + $ifi = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -like 'SeManageVolumePrivilege*' }).substring(26).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + if ($ifi.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Instant File Initialization Rights on $computer" + } + + Write-Message -Level Verbose -Message "Getting Lock Pages in Memory Privileges on $Computer" + $lpim = Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ArgumentList $ResolveSID -ScriptBlock { + Param ($ResolveSID) + . ([ScriptBlock]::Create($ResolveSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + (Get-Content $temp\secpolByDbatools.cfg | Where-Object { $_ -like 'SeLockMemoryPrivilege*' }).substring(24).split(",").replace("`*", "") | + ForEach-Object { Convert-SIDToUserName -SID $_ } + } -ErrorAction SilentlyContinue + + if ($lpim.count -eq 0) { + Write-Message -Level Verbose -Message "No users with Lock Pages in Memory Rights on $computer" + } + $users = @() + $BL + $ifi + $lpim | Select-Object -Unique + $users | ForEach-Object { + [PSCustomObject]@{ + ComputerName = $computer + User = $_ + LogonAsBatchPrivilege = $BL -contains $_ + InstantFileInitializationPrivilege = $ifi -contains $_ + LockPagesInMemoryPrivilege = $lpim -contains $_ + } + } + Write-Message -Level Verbose -Message "Removing secpol file on $computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); Remove-Item $temp\secpolByDbatools.cfg -Force > $NULL } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer + } + } } - } -} +} \ No newline at end of file diff --git a/functions/Get-DbaProcess.ps1 b/functions/Get-DbaProcess.ps1 index 88fc968dbc6d..0b4353818990 100644 --- a/functions/Get-DbaProcess.ps1 +++ b/functions/Get-DbaProcess.ps1 @@ -1,187 +1,192 @@ -Function Get-DbaProcess -{ -<# -.SYNOPSIS -This command displays SQL Server processes. - -.DESCRIPTION -This command displays processes associated with a spid, login, host, program or database. - -.PARAMETER SqlServer -The SQL Server instance. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. - -.PARAMETER Spids -This parameter is auto-populated from -SqlServer. You can specify one or more Spids (including blocking spids) to be displayed. - -.PARAMETER Logins -This parameter is auto-populated from-SqlServer and allows only login names that have active processes. You can specify one or more logins whose processes will be displayed. - -.PARAMETER Hosts -This parameter is auto-populated from -SqlServer and allows only host names that have active processes. You can specify one or more Hosts whose processes will be displayed. - -.PARAMETER Programs -This parameter is auto-populated from -SqlServer and allows only program names that have active processes. You can specify one or more Programs whose processes will be displayed. - -.PARAMETER Databases -This parameter is auto-populated from -SqlServer and allows only database names that have active processes. You can specify one or more Databases whose processes will be displayed. - -.PARAMETER Exclude -This parameter is auto-populated from -SqlServer. You can specify one or more Spids to exclude from being displayed (goes well with Logins). - -Exclude is the last filter to run, so even if a Spid matches, for example, Hosts, if it's listed in Exclude it wil be excluded. - -.PARAMETER Detailed -Provides Detailed information - -.PARAMETER NoSystemSpids -Ignores the System Spids -.NOTES -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaProcess - -.EXAMPLE -Get-DbaProcess -SqlServer sqlserver2014a -Logins base\ctrlb, sa - -Shows information about the processes for base\ctrlb and sa on sqlserver2014a. Uses Windows Authentication to login to sqlserver2014a. - -.EXAMPLE -Get-DbaProcess -SqlServer sqlserver2014a -SqlCredential $credential -Spids 56, 77 - -Shows information about the processes for spid 56 and 57. Uses alternative (SQL or Windows) credentials to login to sqlserver2014a. - -.EXAMPLE -Get-DbaProcess -SqlServer sqlserver2014a -Programs 'Microsoft SQL Server Management Studio' - -Shows information about the processes that were created in Microsoft SQL Server Management Studio. - -.EXAMPLE -Get-DbaProcess -SqlServer sqlserver2014a -Hosts workstationx, server100 - -Shows information about the processes that were initiated by hosts (computers/clients) workstationx and server 1000. - -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [object]$SqlCredential, - [switch]$NoSystemSpids, - [switch]$Detailed - ) - - DynamicParam { if ($sqlserver) { Get-ParamSqlAllProcessInfo -SqlServer $sqlserver -SqlCredential $SqlCredential } } - - BEGIN - { - $sourceserver = Connect-SqlServer -SqlServer $sqlserver -SqlCredential $SqlCredential - $source = $sourceserver.DomainInstanceName - - $logins = $psboundparameters.Logins - $spids = $psboundparameters.Spids - $exclude = $psboundparameters.Exclude - $hosts = $psboundparameters.Hosts - $programs = $psboundparameters.Programs - $databases = $psboundparameters.Databases - } - - PROCESS - { - $allsessions = @() - - $processes = $sourceserver.EnumProcesses() - $servercolumn = $processes.Columns.Add("SqlServer", [object]) - $servercolumn.SetOrdinal(0) - - foreach ($row in $processes) - { - $row["SqlServer"] = $sourceserver - } - - if ($logins.count -gt 0) - { - $allsessions += $processes | Where-Object { $_.Login -in $Logins -and $_.Spid -notin $allsessions.Spid } - } - - if ($spids.count -gt 0) - { - $allsessions += $processes | Where-Object { ($_.Spid -in $spids -or $_.BlockingSpid -in $spids) -and $_.Spid -notin $allsessions.Spid } - } - - if ($hosts.count -gt 0) - { - $allsessions += $processes | Where-Object { $_.Host -in $hosts -and $_.Spid -notin $allsessions.Spid } - } - - if ($programs.count -gt 0) - { - $allsessions += $processes | Where-Object { $_.Program -in $programs -and $_.Spid -notin $allsessions.Spid } - } - - if ($databases.count -gt 0) - { - $allsessions += $processes | Where-Object { $databases -contains $_.Database -and $_.Spid -notin $allsessions.Spid } - } - - # feel like I'm doing this wrong but it's 2am ;) - if ($logins -eq $null -and $spids -eq $null -and $spids -eq $exclude -and $hosts -eq $null -and $programs -eq $null -and $programs -eq $databases) - { - $allsessions = $processes - } - - if ($nosystemspids -eq $true) - { - $allsessions = $allsessions | Where-Object { $_.Spid -gt 50 } - } - - if ($exclude.count -gt 0) - { - $allsessions = $allsessions | Where-Object { $exclude -notcontains $_.SPID -and $_.Spid -notin $allsessions.Spid } - } - - if ($Detailed) - { - $object = ($allsessions | Select-Object SqlServer, Spid, Login, Host, Database, BlockingSpid, Program, @{ - name = "Status"; expression = { - if ($_.Status -eq "") { "sleeping" } - else { $_.Status } - } - }, @{ - name = "Command"; expression = { - if ($_.Command -eq "") { "AWAITING COMMAND" } - else { $_.Command } - } - }, Cpu, MemUsage, IsSystem) - - Select-DefaultView -InputObject $object -Property Spid, Login, Host, Database, BlockingSpid, Program, Status, Command, Cpu, MemUsage, IsSystem - } - else - { - $object = ($allsessions | Select-Object SqlServer, Spid, Login, Host, Database, BlockingSpid, Program, @{ - name = "Status"; expression = { - if ($_.Status -eq "") { "sleeping" } - else { $_.Status } - } - }, @{ - name = "Command"; expression = { - if ($_.Command -eq "") { "AWAITING COMMAND" } - else { $_.Command } - } - }) - - Select-DefaultView -InputObject $object -Property Spid, Login, Host, Database, BlockingSpid, Program, Status, Command - } - } -} +function Get-DbaProcess { + <# + .SYNOPSIS + This command displays SQL Server processes. + + .DESCRIPTION + This command displays processes associated with a spid, login, host, program or database. + + Thanks to Michael J Swart at https://sqlperformance.com/2017/07/sql-performance/find-database-connection-leaks for the query to get the last executed SQL statement, minutesasleep and host process ID. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Spid + Specifies one or more process IDs (Spid) to be displayed. Options for this parameter are auto-populated from the server. + + .PARAMETER Login + Specifies one or more Login names with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Hostname + Specifies one or more hostnames with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Program + Specifies one or more program names with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER Database + Specifies one or more databases with active processes to look for. Options for this parameter are auto-populated from the server. + + .PARAMETER ExcludeSpid + Specifies one ore more process IDs to exclude from display. Options for this parameter are auto-populated from the server. + + This is the last filter to run, so even if a Spid matches another filter, it will be excluded by this filter. + + .PARAMETER NoSystemSpid + If this switch is enabled, system Spids will be ignored. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Process, Session, ActivityMonitor + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaProcess + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Login base\ctrlb, sa + + Shows information about the processes for base\ctrlb and sa on sqlserver2014a. Windows Authentication is used in connecting to sqlserver2014a. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -SqlCredential $credential -Spid 56, 77 + + Shows information about the processes for spid 56 and 57. Uses alternative (SQL or Windows) credentials to authenticate to sqlserver2014a. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Program 'Microsoft SQL Server Management Studio' + + Shows information about the processes that were created in Microsoft SQL Server Management Studio. + + .EXAMPLE + Get-DbaProcess -SqlInstance sqlserver2014a -Host workstationx, server100 + + Shows information about the processes that were initiated by hosts (computers/clients) workstationx and server 1000. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [int[]]$Spid, + [int[]]$ExcludeSpid, + [string[]]$Database, + [string[]]$Login, + [string[]]$Hostname, + [string[]]$Program, + [switch]$NoSystemSpid, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $sqlinstance) { + + Write-Message -Message "Attempting to connect to $instance." -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $instance : $_" -Target $instance -ErrorRecord $_ -Continue + } + + $sql = "SELECT datediff(minute, s.last_request_end_time, getdate()) as MinutesAsleep, s.session_id as spid, s.host_process_id as HostProcessId, t.text as Query, + s.login_time as LoginTime,s.client_version as ClientVersion, s.last_request_start_time as LastRequestStartTime, s.last_request_end_time as LastRequestEndTime, + c.net_transport as NetTransport, c.encrypt_option as EncryptOption, c.auth_scheme as AuthScheme, c.net_packet_size as NetPacketSize, c.client_net_address as ClientNetAddress + FROM sys.dm_exec_connections c join sys.dm_exec_sessions s on c.session_id = s.session_id cross apply sys.dm_exec_sql_text(c.most_recent_sql_handle) t" + + if ($server.VersionMajor -gt 8) { + $results = $server.Query($sql) + } + else { + $results = $null + } + + $allsessions = @() + + $processes = $server.EnumProcesses() + + if ($Login) { + $allsessions += $processes | Where-Object { $_.Login -in $Login -and $_.Spid -notin $allsessions.Spid } + } + + if ($Spid) { + $allsessions += $processes | Where-Object { ($_.Spid -in $Spid -or $_.BlockingSpid -in $Spid) -and $_.Spid -notin $allsessions.Spid } + } + + if ($Hostname) { + $allsessions += $processes | Where-Object { $_.Host -in $Hostname -and $_.Spid -notin $allsessions.Spid } + } + + if ($Program) { + $allsessions += $processes | Where-Object { $_.Program -in $Program -and $_.Spid -notin $allsessions.Spid } + } + + if ($Database) { + $allsessions += $processes | Where-Object { $Database -contains $_.Database -and $_.Spid -notin $allsessions.Spid } + } + + if (Test-Bound -not 'Login', 'Spid', 'ExcludeSpid', 'Hostname', 'Program', 'Database') { + $allsessions = $processes + } + + if ($NoSystemSpid -eq $true) { + $allsessions = $allsessions | Where-Object { $_.Spid -gt 50 } + } + + if ($Exclude) { + $allsessions = $allsessions | Where-Object { $Exclude -notcontains $_.SPID -and $_.Spid -notin $allsessions.Spid } + } + + foreach ($session in $allsessions) { + + if ($session.Status -eq "") { + $status = "sleeping" + } + else { + $status = $session.Status + } + + if ($session.Command -eq "") { + $command = "AWAITING COMMAND" + } + else { + $command = $session.Command + } + + $row = $results | Where-Object { $_.Spid -eq $session.Spid } + + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Parent -value $server + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Status -value $status + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name Command -value $command + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name HostProcessId -value $row.HostProcessId + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name MinutesAsleep -value $row.MinutesAsleep + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LoginTime -value $row.LoginTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ClientVersion -value $row.ClientVersion + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastRequestStartTime -value $row.LastRequestStartTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastRequestEndTime -value $row.LastRequestEndTime + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name NetTransport -value $row.NetTransport + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name EncryptOption -value $row.EncryptOption + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name AuthScheme -value $row.AuthScheme + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name NetPacketSize -value $row.NetPacketSize + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name ClientNetAddress -value $row.ClientNetAddress + Add-Member -Force -InputObject $session -MemberType NoteProperty -Name LastQuery -value $row.Query + + Select-DefaultView -InputObject $session -Property ComputerName, InstanceName, SqlInstance, Spid, Login, LoginTime, Host, Database, BlockingSpid, Program, Status, Command, Cpu, MemUsage, LastRequestStartTime, LastRequestEndTime, MinutesAsleep, ClientNetAddress, NetTransport, EncryptOption, AuthScheme, NetPacketSize, ClientVersion, HostProcessId, IsSystem, LastQuery + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaQueryExecutionTime.ps1 b/functions/Get-DbaQueryExecutionTime.ps1 new file mode 100644 index 000000000000..67d68b8fe91a --- /dev/null +++ b/functions/Get-DbaQueryExecutionTime.ps1 @@ -0,0 +1,247 @@ +function Get-DbaQueryExecutionTime { + <# +.SYNOPSIS +Displays Stored Procedures and Ad hoc queries with the highest execution times. Works on SQL Server 2008 and above. + +.DESCRIPTION +Quickly find slow query executions within a database. Results will include stored procedures and individual SQL statements. + +.PARAMETER SqlInstance +Allows you to specify a comma separated list of servers to query. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER MaxResultsPerDb +Allows you to limit the number of results returned, as many systems can have very large amounts of query plans. Default value is 100 results. + +.PARAMETER MinExecs +Allows you to limit the scope to queries that have been executed a minimum number of time. Default value is 100 executions. + +.PARAMETER MinExecMs +Allows you to limit the scope to queries with a specified average execution time. Default value is 500 (ms). + +.PARAMETER NoSystemDb +Allows you to suppress output on system databases + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Query, Performance +Author: Brandon Abshire, netnerds.net + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaQueryExecutionTime + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008, sqlserver2012 + +Return the top 100 slowest stored procedures or statements for servers sql2008 and sqlserver2012. + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008 -Database TestDB + +Return the top 100 slowest stored procedures or statements on server sql2008 for only the TestDB database. + +.EXAMPLE +Get-DbaQueryExecutionTime -SqlInstance sql2008 -Database TestDB -MaxResultsPerDb 100 -MinExecs 200 -MinExecMs 1000 + +Return the top 100 slowest stored procedures or statements on server sql2008 for only the TestDB database, +limiting results to queries with more than 200 total executions and an execution time over 1000ms or higher. +#> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Position = 1, Mandatory = $false)] + [int]$MaxResultsPerDb = 100, + [parameter(Position = 2, Mandatory = $false)] + [int]$MinExecs = 100, + [parameter(Position = 3, Mandatory = $false)] + [int]$MinExecMs = 500, + [parameter(Position = 4, Mandatory = $false)] + [switch]$NoSystemDb, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = ";With StatsCTE AS + ( + SELECT + DB_NAME() as DatabaseName, + (total_worker_time / execution_count) / 1000 AS AvgExec_ms , + execution_count , + max_worker_time / 1000 AS MaxExec_ms , + OBJECT_NAME(object_id) as ProcName, + object_id, + type_desc, + cached_time, + last_execution_time, + total_worker_time / 1000 as total_worker_time_ms, + total_elapsed_time / 1000 as total_elapsed_time_ms, + OBJECT_NAME(object_id) as SQLText, + OBJECT_NAME(object_id) as full_statement_text + FROM sys.dm_exec_procedure_stats + WHERE database_id = DB_ID()" + + If ($MinExecs) { $sql += "`n AND execution_count >= " + $MinExecs } + If ($MinExecMs) { $sql += "`n AND (total_worker_time / execution_count) / 1000 >= " + $MinExecMs } + + $sql += "`n UNION + SELECT + DB_NAME() as DatabaseName, + ( qs.total_worker_time / qs.execution_count ) / 1000 AS AvgExec_ms , + qs.execution_count , + qs.max_worker_time / 1000 AS MaxExec_ms , + OBJECT_NAME(st.objectid) as ProcName, + st.objectid as [object_id], + 'STATEMENT' as type_desc, + '1901-01-01 00:00:00' as cached_time, + qs.last_execution_time, + qs.total_worker_time / 1000 as total_worker_time_ms, + qs.total_elapsed_time / 1000 as total_elapsed_time_ms, + SUBSTRING(st.text, (qs.statement_start_offset/2)+1, 50) + '...' AS SQLText, + SUBSTRING(st.text, (qs.statement_start_offset/2)+1, + ((CASE qs.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END - qs.statement_start_offset)/2) + 1) AS full_statement_text + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) as pa + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as st + WHERE st.dbid = DB_ID() OR (pa.attribute = 'dbid' and pa.value = DB_ID())" + + If ($MinExecs) { $sql += "`n AND execution_count >= " + $MinExecs } + If ($MinExecMs) { $sql += "`n AND (total_worker_time / execution_count) / 1000 >= " + $MinExecMs } + + If ($MaxResultsPerDb) { $sql += ")`n SELECT TOP " + $MaxResultsPerDb } + Else { + $sql += ") + SELECT " + } + + $sql += "`n DatabaseName, + AvgExec_ms, + execution_count, + MaxExec_ms, + ProcName, + object_id, + type_desc, + cached_time, + last_execution_time, + total_worker_time_ms, + total_elapsed_time_ms, + SQLText, + full_statement_text + FROM StatsCTE " + + If ($MinExecs -or $MinExecMs) { + $sql += "`n WHERE `n" + + If ($MinExecs) { + $sql += " execution_count >= " + $MinExecs + } + + If ($MinExecMs -gt 0 -and $MinExecs) { + $sql += "`n AND AvgExec_ms >= " + $MinExecMs + } + elseif ($MinExecMs) { + $sql += "`n AvgExecs_ms >= " + $MinExecMs + } + } + + $sql += "`n ORDER BY AvgExec_ms DESC" + } + process { + if (!$MaxResultsPerDb -and !$MinExecs -and !$MinExecMs) { + Write-Warning "Results may take time, depending on system resources and size of buffer cache." + Write-Warning "Consider limiting results using -MaxResultsPerDb, -MinExecs and -MinExecMs parameters." + } + + foreach ($instance in $SqlInstance) { + Write-Verbose "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.versionMajor -lt 10) { + Write-Warning "This function does not support versions lower than SQL Server 2008 (v10). Skipping server $instance." + + Continue + } + + $dbs = $server.Databases + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($NoSystemDb) { + $dbs = $dbs | Where-Object { $_.IsSystemObject -eq $false } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Verbose "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Write-Warning "The database $db is not accessible. Skipping database." + Continue + } + + try { + foreach ($row in $db.ExecuteWithResults($sql).Tables.Rows) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DatabaseName + ProcName = $row.ProcName + ObjectID = $row.object_id + TypeDesc = $row.type_desc + Executions = $row.Execution_Count + AvgExecMs = $row.AvgExec_ms + MaxExecMs = $row.MaxExec_ms + CachedTime = $row.cached_time + LastExecTime = $row.last_execution_time + TotalWorkerTimeMs = $row.total_worker_time_ms + TotalElapsedTimeMs = $row.total_elapsed_time_ms + SQLText = $row.SQLText + FullStatementText = $row.full_statement_text + } | Select-DefaultView -ExcludeProperty FullStatementText + } + } + catch { + Write-Warning "Could not process $db on $instance. Exception: $_" + Continue + } + } + } + } +} diff --git a/functions/Get-DbaQueryStoreConfig.ps1 b/functions/Get-DbaQueryStoreConfig.ps1 deleted file mode 100644 index a7e5f8a762d6..000000000000 --- a/functions/Get-DbaQueryStoreConfig.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -Function Get-DbaQueryStoreConfig -{ -<# -.SYNOPSIS -Get the Query Store configuration for Query Store enabled databases. - -.DESCRIPTION -Retrieves and returns the Query Store configuration for every database that has the Query Store feature enabled. - -.PARAMETER SqlInstance -The SQL Server that you're connecting to. - -.PARAMETER SqlCredential -SqlCredential object used to connect to the SQL Server as a different user. - -.PARAMETER Databases -Return information for only specific databases - -.PARAMETER Exclude -Return information for all but these specific databases - -.NOTES -Author: Enrico van de Laar ( @evdlaar ) -Tags: QueryStore -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK -https://dbatools.io/Get-QueryStoreConfig - -.EXAMPLE -Get-DbaQueryStoreConfig -SqlServer ServerA\sql - -Returns Query Store configuration settings for every database on the ServerA\sql instance. - -.EXAMPLE -Get-DbaQueryStoreConfig -SqlServer ServerA\sql | Where-Object {$_.ActualState -eq "ReadWrite"} - -Returns the Query Store configuration for all databases on ServerA\sql where the Query Store feature is in Read/Write mode. - -.EXAMPLE -Get-DbaQueryStoreConfig -SqlServer localhost | format-table -AutoSize -Wrap - -Returns Query Store configuration settings for every database on the ServerA\sql instance inside a table format. - -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [object[]]$SqlInstance, - [System.Management.Automation.PSCredential]$SqlCredential - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $SqlCredential - } - } - - BEGIN - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - } - - PROCESS - { - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $SqlCredential - - } - catch - { - Write-Warning "Can't connect to $instance. Moving on." - continue - } - - if ($server.VersionMajor -lt 13) - { - Write-Warning "The SQL Server Instance ($instance) has a lower SQL Server version than SQL Server 2016. Skipping server." - continue - } - - - # We have to exclude all the system databases since they cannot have the Query Store feature enabled - $dbs = $server.Databases | Where-Object { $_.IsSystemObject -eq $false } - - if ($databases.count -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - - if ($exclude.count -gt 0) - { - $dbs = $dbs | Where-Object { $exclude -notcontains $_.Name } - } - - - foreach ($db in $dbs) - { - Write-Verbose "Processing $db on $instance" - - if ($db.IsAccessible -eq $false) - { - Write-Warning "The database $db on server $instance is not accessible. Skipping database." - Continue - } - - [pscustomobject]@{ - Instance = $instance.name - Database = $db.name - ActualState = $db.QueryStoreOptions.ActualState - FlushInterval = $db.QueryStoreOptions.DataFlushIntervalInSeconds - CollectionInterval = $db.QueryStoreOptions.StatisticsCollectionIntervalInMinutes - MaxSize = $db.QueryStoreOptions.MaxStorageSizeInMB - CurrentSize = $db.QueryStoreOptions.CurrentStorageSizeInMB - CaptureMode = $db.QueryStoreOptions.QueryCaptureMode - CleanupMode = $db.QueryStoreOptions.SizeBasedCleanupMode - StaleQueryThreshold = $db.QueryStoreOptions.StaleQueryThresholdInDays - } - } - } - } -} diff --git a/functions/Get-DbaRegisteredServer.ps1 b/functions/Get-DbaRegisteredServer.ps1 new file mode 100644 index 000000000000..eb88150894d0 --- /dev/null +++ b/functions/Get-DbaRegisteredServer.ps1 @@ -0,0 +1,208 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaRegisteredServer { + <# + .SYNOPSIS + Gets list of SQL Server objects stored in SQL Server Central Management Server (CMS). + + .DESCRIPTION + Returns an array of servers found in the CMS. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Group + Specifies one or more groups to include from SQL Server Central Management Server. + + .PARAMETER ExcludeGroup + Specifies one or more Central Management Server groups to exclude. + + .PARAMETER ExcludeCmsServer + Deprecated, now follows the Microsoft convention of not including it by default. If you'd like to include the CMS Server, use -IncludeSelf + + .PARAMETER IncludeSelf + If this switch is enabled, the CMS server itself will be included in the results, along with all other Registered Servers. + + .PARAMETER ResolveNetworkName + If this switch is enabled, the NetBIOS name and IP address(es) of each server will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Bryan Hamby (@galador) + Tags: RegisteredServer, CMS + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServer + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a + + Gets a list of servers from the CMS on sqlserver2014a, using Windows Credentials. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -IncludeSelf + + Gets a list of servers from the CMS on sqlserver2014a and includes sqlserver2014a in the output results. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -SqlCredential $credential | Select-Object -Unique -ExpandProperty ServerName + + Returns only the server names from the CMS on sqlserver2014a, using SQL Authentication to authenticate to the server. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR, Accounting + + Gets a list of servers in the HR and Accounting groups from the CMS on sqlserver2014a. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR\Development + + Returns a list of servers in the HR and sub-group Development from the CMS on sqlserver2014a. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Groups")] + [object[]]$Group, + [object[]]$ExcludeGroup, + [switch]$IncludeSelf, + [switch]$ExcludeCmsServer, + [switch]$ResolveNetworkName, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + function Find-CmsGroup { + [OutputType([object[]])] + [cmdletbinding()] + param( + $CmsGrp, + $Base = $null, + $Stopat + ) + $results = @() + foreach ($el in $CmsGrp) { + if ($null -eq $Base -or [string]::IsNullOrWhiteSpace($Base) ) { + $partial = $el.name + } + else { + $partial = "$Base\$($el.name)" + } + if ($partial -eq $Stopat) { + return $el + } + else { + foreach ($elg in $el.ServerGroups) { + $results += Find-CmsGroup -CmsGrp $elg -Base $partial -Stopat $Stopat + } + } + } + return $results + } + + $defaults = @() + if ($ResolveNetworkName) { + $defaults += 'ComputerName', 'FQDN', 'IPAddress' + } + $defaults += 'Name', 'ServerName', 'Description', 'ServerType', 'SecureConnectionString' + + } + + process { + if (Test-FunctionInterrupt) { + return + } + + $servers = @() + foreach ($instance in $SqlInstance) { + try { + $cmsStore = Get-DbaRegisteredServersStore -SqlInstance $instance -SqlCredential $SqlCredential -EnableException + } + catch { + Stop-Function -Message "Cannot access Central Management Server '$instance'." -ErrorRecord $_ -Continue + } + + if (Test-Bound -ParameterName ExcludeGroup) { + $Group = ($cmsStore.DatabaseEngineServerGroup.ServerGroups | Where-Object Name -notin $ExcludeGroup).Name + } + + if ($Group) { + foreach ($currentGroup in $Group) { + $cms = Find-CmsGroup -CmsGrp $cmsStore.DatabaseEngineServerGroup.ServerGroups -Stopat $currentGroup + if ($null -eq $cms) { + Write-Message -Level Output -Message "No groups found matching that name on instance '$instance'." + continue + } + $servers += ($cms.GetDescendantRegisteredServers()) + } + } + else { + $cms = $cmsStore.DatabaseEngineServerGroup + $servers += ($cms.GetDescendantRegisteredServers()) + } + if ($Group -and (Test-Bound -ParameterName Group -Not)) { + #add root ones + $servers += ($cmsstore.DatabaseEngineServerGroup.RegisteredServers) + } + + # Close the connection, otherwise using it with the ServersStore will keep it open + $cmsStore.ServerConnection.Disconnect() + } + + foreach ($server in $servers) { + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $null + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $null + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $null + + if ($ResolveNetworkName) { + try { + $lookup = Resolve-DbaNetworkName $server.ServerName -Turbo + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $lookup.ComputerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $lookup.FQDN + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $lookup.IPAddress + } + catch { + try { + $lookup = Resolve-DbaNetworkName $server.ServerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name ComputerName -Value $lookup.ComputerName + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name FQDN -Value $lookup.FQDN + Add-Member -Force -InputObject $server -MemberType NoteProperty -Name IPAddress -Value $lookup.IPAddress + } + catch {} + } + } + + Add-Member -Force -InputObject $server -MemberType ScriptMethod -Name ToString -Value { $this.ServerName } + Select-DefaultView -InputObject $server -Property $defaults + } + + if ($IncludeSelf -and $servers) { + $self = $servers[0].PsObject.Copy() + $self | Add-Member -MemberType NoteProperty -Name Name -Value "CMS Instance" -Force + $self.ServerName = $instance + $self.Description = $null + $self.SecureConnectionString = $null + Select-DefaultView -InputObject $self -Property $defaults + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Parameter ExcludeCmsServer + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaRegisteredServerName + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-SqlRegisteredServerName + } +} diff --git a/functions/Get-DbaRegisteredServerGroup.ps1 b/functions/Get-DbaRegisteredServerGroup.ps1 new file mode 100644 index 000000000000..6083178ad011 --- /dev/null +++ b/functions/Get-DbaRegisteredServerGroup.ps1 @@ -0,0 +1,140 @@ +function Get-DbaRegisteredServerGroup { + <# + .SYNOPSIS + Gets list of Server Groups objects stored in SQL Server Central Management Server (CMS). + + .DESCRIPTION + Returns an array of Server Groups found in the CMS. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Group + Specifies one or more groups to include from SQL Server Central Management Server. + + .PARAMETER ExcludeGroup + Specifies one or more Central Management Server groups to exclude. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Tony Wilhelm (@tonywsql) + Tags: RegisteredServer, CMS + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServerGroup + + .EXAMPLE + Get-DbaRegisteredServerGroup -SqlInstance sqlserver2014a + + Gets the top level groups from the CMS on sqlserver2014a, using Windows Credentials. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -SqlCredential $credential + + Gets the top level groups from the CMS on sqlserver2014a, using SQL Authentication to authenticate to the server. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR, Accounting + + Gets the HR and Accounting groups from the CMS on sqlserver2014a. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a -Group HR\Development + + Returns the sub-group Development of the HR group from the CMS on sqlserver2014a. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Group, + [object[]]$ExcludeGroup, + [switch]$EnableException + ) + begin { + function Find-CmsGroup { + [OutputType([object[]])] + [cmdletbinding()] + param( + $CmsGrp, + $Base = $null, + $Stopat + ) + $results = @() + + foreach ($el in $CmsGrp) { + if ($null -eq $Base -or [string]::IsNullOrWhiteSpace($Base) ) { + $partial = $el.name + } + else { + $partial = "$Base\$($el.name)" + } + if ($partial -eq $Stopat) { + return $el + } + else { + foreach ($elg in $el.ServerGroups) { + $results += Find-CmsGroup -CmsGrp $elg -Base $partial -Stopat $Stopat + } + } + } + return $results + } + } + + process { + + if (Test-FunctionInterrupt) { + return + } + + $groups = @() + foreach ($instance in $SqlInstance) { + + try { + $cmsStore = Get-DbaRegisteredServersStore -SqlInstance $instance -SqlCredential $SqlCredential -EnableException + } + catch { + Stop-Function -Message "Cannot access Central Management Server '$instance'." -ErrorRecord $_ -Continue + } + + if ($group) { + foreach ($currentGroup in $group) { + $cms = Find-CmsGroup -CmsGrp $cmsStore.DatabaseEngineServerGroup.ServerGroups -Stopat $currentGroup + if ($null -eq $cms) { + Write-Message -Level Output -Message "No groups found matching '$($currentGroup)' on instance '$instance'." + continue + } + $groups += $cms + } + } + else { + $groups = $cmsStore.DatabaseEngineServerGroup.ServerGroups + } + + if (Test-Bound -ParameterName ExcludeGroup) { + $groups = $groups | Where-Object Name -notin $ExcludeGroup + } + + # Close the connection, otherwise using it with the ServersStore will keep it open + $cmsStore.ServerConnection.Disconnect() + + $groups | Select-DefaultView -ExcludeProperty IsLocal, IsSystemServerGroup, IsDropped, Urn, Properties, Metadata, DuplicateFound, PropertyMetadataChanged, PropertyChanged + } + } +} diff --git a/functions/Get-DbaRegisteredServersStore.ps1 b/functions/Get-DbaRegisteredServersStore.ps1 new file mode 100644 index 000000000000..f208bd5963af --- /dev/null +++ b/functions/Get-DbaRegisteredServersStore.ps1 @@ -0,0 +1,72 @@ +function Get-DbaRegisteredServersStore { + <# + .SYNOPSIS + Returns a SQL Server Registered Server Store Object + + .DESCRIPTION + Returns a SQL Server Registered Server Store object - useful for working with Central Management Store + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: RegisteredServer,CMS + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRegisteredServersStore + + .EXAMPLE + Get-DbaRegisteredServersStore -SqlInstance sqlserver2014a + + Returns a SQL Server Registered Server Store Object from sqlserver2014a + + .EXAMPLE + Get-DbaRegisteredServersStore -SqlInstance sqlserver2014a -SqlCredential (Get-Credential sqladmin) + + Returns a SQL Server Registered Server Store Object from sqlserver2014a by logging in with the sqladmin login + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($Instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $store = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($server.ConnectionContext.SqlConnectionObject) + } + catch { + Stop-Function -Message "Cannot access Central Management Server on $instance." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $store -ExcludeProperty ServerConnection, DomainInstanceName, DomainName, Urn, Properties, Metadata, Parent, ConnectionContext, PropertyMetadataChanged, PropertyChanged + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaResourceGovernorClassifierFunction.ps1 b/functions/Get-DbaResourceGovernorClassifierFunction.ps1 new file mode 100644 index 000000000000..3493cf5a03b9 --- /dev/null +++ b/functions/Get-DbaResourceGovernorClassifierFunction.ps1 @@ -0,0 +1,80 @@ +function Get-DbaResourceGovernorClassifierFunction { +<# +.SYNOPSIS +Gets the Resource Governor custom classifier Function + +.DESCRIPTION +Gets the Resource Governor custom classifier Function which is used for customize the workload groups usage + +.PARAMETER SqlInstance +The target SQL Server instance(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER EnableException +By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. +This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. +Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Migration, ResourceGovernor +Author: Alessandro Alpi (@suxstellino), alessandroalpi.blog +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaResourceGovernorClassifierFunction + +.EXAMPLE +Get-DbaResourceGovernorClassifierFunction -SqlInstance sql2016 + +Gets the classifier function object of the SqlInstance sql2016 + +.EXAMPLE +'Sql1','Sql2/sqlexpress' | Get-DbaResourceGovernorClassifierFunction + +Gets the classifier function object on Sql1 and Sql2/sqlexpress instances + +#> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $classifierFunction = $null + + foreach ($currentFunction in $server.Databases["master"].UserDefinedFunctions) + { + $fullyQualifiedFunctionName = [string]::Format("[{0}].[{1}]", $currentFunction.Schema, $currentFunction.Name) + if ($fullyQualifiedFunctionName -eq $server.ResourceGovernor.ClassifierFunction) + { + $classifierFunction = $currentFunction + } + } + + if ($classifierFunction) { + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $classifierFunction -MemberType NoteProperty -Name Database -value 'master' + } + + Select-DefaultView -InputObject $classifierFunction -Property ComputerName, InstanceName, SqlInstance, Database, Schema, CreateDate, DateLastModified, Name, DataType + } + } +} diff --git a/functions/Get-DbaRestoreHistory.ps1 b/functions/Get-DbaRestoreHistory.ps1 index 7b36643ab3ca..c4ab4f4cc456 100644 --- a/functions/Get-DbaRestoreHistory.ps1 +++ b/functions/Get-DbaRestoreHistory.ps1 @@ -1,197 +1,204 @@ -Function Get-DbaRestoreHistory -{ -<# -.SYNOPSIS -Returns restore history details for databases on a SQL Server - -.DESCRIPTION -By default, this command will return the server name, database, username, restore type, date, from file and to files. - -Thanks to https://www.mssqltips.com/sqlservertip/1724/when-was-the-last-time-your-sql-server-database-was-restored/ for the query and https://sqlstudies.com/2016/07/27/when-was-this-database-restored/ for the idea. - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Databases -Return restore information for only specific databases. These are only the databases that currently exist on the server. - -.PARAMETER Exclude -Return restore information for all but these specific databases - -.PARAMETER Since -Datetime object used to narrow the results to a date - -.PARAMETER Force -Returns a ton of information about the backup history with no max rows - -.NOTES -Tags: DisasterRecovery, Backup, Restore -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaRestoreHistory - -.EXAMPLE -Get-DbaRestoreHistory -SqlServer sqlserver2014a - -Returns server name, database, username, restore type, date for all restored databases on sqlserver2014a. - -.EXAMPLE -Get-DbaRestoreHistory -SqlServer sqlserver2014a -Databases db1, db2 -Since '7/1/2016 10:47:00' - -Returns restore information only for databases db1 and db2 on sqlserve2014a since July 1, 2016 at 10:47 AM. - -.EXAMPLE -Get-DbaRestoreHistory -SqlServer sqlserver2014a, sql2016 -Exclude db1 - -Lots of detailed information for all databases except db1 on sqlserver2014a and sql2016 - -.EXAMPLE -Get-DbaRestoreHistory -SqlServer sql2014 -Databases AdventureWorks2014, pubs | Format-Table - -Adds From and To file information to output, returns information only for AdventureWorks2014 and pubs, and makes the output pretty - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sql2016 | Get-DbaRestoreHistory - -Returns database restore information for every database on every server listed in the Central Management Server on sql2016 - -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [string[]]$SqlServer, - [Alias("SqlCredential")] - [PsCredential]$Credential, - [datetime]$Since, - [switch]$Force - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabases -SqlServer $SqlServer[0] -SqlCredential $Credential } } - - BEGIN - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - - if ($Since -ne $null) - { - $Since = $Since.ToString("yyyy-MM-dd HH:mm:ss") - } - } - - PROCESS - { - foreach ($instance in $SqlServer) - { - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - - if ($server.VersionMajor -lt 9) - { - Write-Warning "SQL Server 2000 not supported" - continue - } - - $computername = $server.NetName - $instancename = $server.ServiceName - $servername = $server.DomainInstanceName - - if ($force -eq $true) - { - $select = "SELECT '$computername' AS [ComputerName], - '$instancename' AS [InstanceName], - '$servername' AS [SqlInstance], * " - } - else - { - $select = "SELECT - '$computername' AS [ComputerName], - '$instancename' AS [InstanceName], - '$servername' AS [SqlInstance], - rsh.destination_database_name AS [Database], - --rsh.restore_history_id as RestoreHistoryID, - rsh.user_name AS [Username], - CASE - WHEN rsh.restore_type = 'D' THEN 'Database' - WHEN rsh.restore_type = 'F' THEN 'File' - WHEN rsh.restore_type = 'G' THEN 'Filegroup' - WHEN rsh.restore_type = 'I' THEN 'Differential' - WHEN rsh.restore_type = 'L' THEN 'Log' - WHEN rsh.restore_type = 'V' THEN 'Verifyonly' - WHEN rsh.restore_type = 'R' THEN 'Revert' - ELSE rsh.restore_type - END AS [RestoreType], - rsh.restore_date AS [Date], - ISNULL(STUFF((SELECT ', ' + bmf.physical_device_name - FROM msdb.dbo.backupmediafamily bmf - WHERE bmf.media_set_id = bs.media_set_id - FOR XML PATH('')), 1, 2, ''), '') AS [From], - ISNULL(STUFF((SELECT ', ' + rf.destination_phys_name - FROM msdb.dbo.restorefile rf - WHERE rsh.restore_history_id = rf.restore_history_id - FOR XML PATH('')), 1, 2, ''), '') AS [To] - " - } - - $from = " FROM msdb.dbo.restorehistory rsh - INNER JOIN msdb.dbo.backupset bs ON rsh.backup_set_id = bs.backup_set_id" - - if ($exclude.length -gt 0 -or $databases.length -gt 0 -or $Since.length -gt 0) - { - $where = " WHERE " - } - - $wherearray = @() - - if ($exclude.length -gt 0) - { - $dblist = $exclude -join "','" - $wherearray += " destination_database_name not in ('$dblist')" - } - - if ($databases.length -gt 0) - { - $dblist = $databases -join "','" - $wherearray += "destination_database_name in ('$dblist')" - } - - if ($Since -ne $null) - { - $wherearray += "rsh.restore_date >= '$since'" - } - - if ($where.length -gt 0) - { - $wherearray = $wherearray -join " and " - $where = "$where $wherearray" - } - - $sql = "$select $from $where" - Write-Debug $sql - - $server.ConnectionContext.ExecuteWithResults($sql).Tables.Rows - - } - catch - { - Write-Warning $_ - Write-Exception $_ - continue - } - } - } +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaRestoreHistory { + <# + .SYNOPSIS + Returns restore history details for databases on a SQL Server. + + .DESCRIPTION + By default, this command will return the server name, database, username, restore type, date, from file and to files. + + Thanks to https://www.mssqltips.com/SqlInstancetip/1724/when-was-the-last-time-your-sql-server-database-was-restored/ for the query and https://sqlstudies.com/2016/07/27/when-was-this-database-restored/ for the idea. + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to operate on. Requires SQL Server 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER Since + Specifies a datetime to use as the starting point for searching backup history. + + .PARAMETER Force + Deprecated. + + .PARAMETER Last + If this switch is enabled, the last restore action performed on each database is returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaRestoreHistory + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2016 + + Returns server name, database, username, restore type, date for all restored databases on sql2016. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2016 -Database db1, db2 -Since '7/1/2016 10:47:00' + + Returns restore information only for databases db1 and db2 on sql2016 since July 1, 2016 at 10:47 AM. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2014, sql2016 -Exclude db1 + + Lots of detailed information for all databases except db1 on sql2014 and sql2016. + + .EXAMPLE + Get-DbaRestoreHistory -SqlInstance sql2014 -Database AdventureWorks2014, pubs | Format-Table + + Adds From and To file information to output, returns information only for AdventureWorks2014 and pubs, and formats the data as a table. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2016 | Get-DbaRestoreHistory + + Returns database restore information for every database on every server listed in the Central Management Server on sql2016. + + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$Since, + [switch]$Force, + [switch]$Last, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0.0" -EnableException:$false -Parameter 'Force' + + if ($Since -ne $null) { + $Since = $Since.ToString("yyyy-MM-ddTHH:mm:ss") + } + } + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + $computername = $server.NetName + $instancename = $server.ServiceName + $servername = $server.DomainInstanceName + + if ($force -eq $true) { + $select = "SELECT '$computername' AS [ComputerName], + '$instancename' AS [InstanceName], + '$servername' AS [SqlInstance], * " + } + else { + $select = "SELECT + '$computername' AS [ComputerName], + '$instancename' AS [InstanceName], + '$servername' AS [SqlInstance], + rsh.destination_database_name AS [Database], + --rsh.restore_history_id as RestoreHistoryID, + rsh.user_name AS [Username], + CASE + WHEN rsh.restore_type = 'D' THEN 'Database' + WHEN rsh.restore_type = 'F' THEN 'File' + WHEN rsh.restore_type = 'G' THEN 'Filegroup' + WHEN rsh.restore_type = 'I' THEN 'Differential' + WHEN rsh.restore_type = 'L' THEN 'Log' + WHEN rsh.restore_type = 'V' THEN 'Verifyonly' + WHEN rsh.restore_type = 'R' THEN 'Revert' + ELSE rsh.restore_type + END AS [RestoreType], + rsh.restore_date AS [Date], + ISNULL(STUFF((SELECT ', ' + bmf.physical_device_name + FROM msdb.dbo.backupmediafamily bmf + WHERE bmf.media_set_id = bs.media_set_id + FOR XML PATH('')), 1, 2, ''), '') AS [From], + ISNULL(STUFF((SELECT ', ' + rf.destination_phys_name + FROM msdb.dbo.restorefile rf + WHERE rsh.restore_history_id = rf.restore_history_id + FOR XML PATH('')), 1, 2, ''), '') AS [To], + bs.first_lsn, + bs.last_lsn, + bs.checkpoint_lsn, + bs.database_backup_lsn, + bs.backup_finish_date, + bs.backup_finish_date AS BackupFinishDate + " + } + + $from = " FROM msdb.dbo.restorehistory rsh + INNER JOIN msdb.dbo.backupset bs ON rsh.backup_set_id = bs.backup_set_id" + + if ($ExcludeDatabase -or $Database -or $Since -or $last) { + $where = " WHERE " + } + + $wherearray = @() + + if ($ExcludeDatabase) { + $dblist = $ExcludeDatabase -join "','" + $wherearray += " destination_database_name not in ('$dblist')" + } + + if ($Database) { + $dblist = $Database -join "','" + $wherearray += "destination_database_name in ('$dblist')" + } + + if ($null -ne $Since) { + $wherearray += "rsh.restore_date >= '$since'" + } + + + if ($last) { + $wherearray += "rsh.backup_set_id in + (select max(backup_set_id) from msdb.dbo.restorehistory + group by destination_database_name + )" + } + + if ($where.length -gt 0) { + $wherearray = $wherearray -join " and " + $where = "$where $wherearray" + } + + $sql = "$select $from $where" + + Write-Message -Level Debug -Message $sql + + $results = $server.ConnectionContext.ExecuteWithResults($sql).Tables.Rows + if ($last) { + $ga = $results | Group-Object Database + $tmpres = @() + foreach($g in $ga) { + $tmpres += $g.Group | Sort-Object -Property Date -Descending | Select-Object -First 1 + } + $results = $tmpres + } + $results | Select-DefaultView -ExcludeProperty first_lsn, last_lsn, checkpoint_lsn, database_backup_lsn, backup_finish_date + } + catch { + Stop-Function -Message "Failure" -Target $SqlInstance -Error $_ -Exception $_.Exception.InnerException -Continue + } + } + } } diff --git a/functions/Get-DbaRoleMember.ps1 b/functions/Get-DbaRoleMember.ps1 index 997df7619fea..79fb2c4f1170 100644 --- a/functions/Get-DbaRoleMember.ps1 +++ b/functions/Get-DbaRoleMember.ps1 @@ -1,6 +1,5 @@ -Function Get-DbaRoleMember -{ -<# +function Get-DbaRoleMember { + <# .SYNOPSIS Get members of all roles on a Sql instance. @@ -13,11 +12,13 @@ Default output includes columns SQLServer, Database, Role, Member. The SQL Server that you're connecting to. .PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. -SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server .PARAMETER IncludeServerLevel Shows also information on Server Level Permissions. @@ -29,140 +30,120 @@ Excludes all members of fixed roles. Credential object used to connect to the SQL Server as a different user. .NOTES +Tags: Role, Database, Security, Login Author: Klaas Vandenberghe ( @PowerDBAKlaas ) -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Get-DbaRoleMember .EXAMPLE -Get-DbaRoleMember -SqlServer ServerA +Get-DbaRoleMember -SqlInstance ServerA Returns a custom object displaying SQLServer, Database, Role, Member for all DatabaseRoles. .EXAMPLE -Get-DbaRoleMember -SqlServer sql2016 | Out-Gridview +Get-DbaRoleMember -SqlInstance sql2016 | Out-Gridview Returns a gridview displaying SQLServer, Database, Role, Member for all DatabaseRoles. .EXAMPLE -Get-DbaRoleMember -SqlServer ServerA\sql987 -IncludeServerLevel +Get-DbaRoleMember -SqlInstance ServerA\sql987 -IncludeServerLevel Returns a gridview displaying SQLServer, Database, Role, Member for both ServerRoles and DatabaseRoles. #> - [CmdletBinding()] - Param ( - [parameter(Mandatory, ValueFromPipeline)] - [Alias('SqlServer', 'Server', 'Instance')] - [string[]]$SqlInstance, - [PsCredential]$SqlCredential, - [switch]$IncludeServerLevel, - [switch]$NoFixedRole - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $SqlCredential - } - } - - BEGIN - { - $functionName = (Get-PSCallstack)[0].Command - $databases = $psboundparameters.Databases - } - - PROCESS - { - foreach ($instance in $sqlinstance) - { - Write-Verbose "$functionName - Connecting to $Instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $sqlcredential - } - catch - { - Write-Warning "$functionName - Failed to connect to $instance" - continue - } - - if ($IncludeServerLevel) - { - Write-Verbose "$functionName - Server Role Members included" - $instroles = $null - Write-Verbose "$functionName - Getting Server Roles on $instance" - $instroles = $server.roles - if ($NoFixedRole) - { - $instroles = $instroles | Where-Object { $_.isfixedrole -eq $false } - } - ForEach ($instrole in $instroles) - { - Write-Verbose "$functionName - Getting Server Role Members for $instrole on $instance" - $irmembers = $null - $irmembers = $instrole.enumserverrolemembers() - ForEach ($irmem in $irmembers) - { - [PSCustomObject]@{ - SQLInstance = $instance - Database = $null - Role = $instrole.name - Member = $irmem.tostring() - } - } - } - } - - $dbs = $server.Databases - - if ($databases.count -gt 0) - { - Write-Verbose "$functionName - $($databases.count) databases on $instance" - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - - foreach ($db in $dbs) - { - Write-Verbose "$functionName - Checking accessibility of $db on $instance" - - if ($db.IsAccessible -ne $true) - { - Write-Warning "$functionName - Database $db on $instance is not accessible" - continue - } - - $dbroles = $db.roles - Write-Verbose "$functionName - Getting Database Roles for $db on $instance" - - if ($NoFixedRole) - { - $dbroles = $dbroles | Where-Object { $_.isfixedrole -eq $false } - } - - foreach ($dbrole in $dbroles) - { - Write-Verbose "$functionName - Getting Database Role Members for $dbrole in $db on $instance" - $dbmembers = $dbrole.enummembers() - ForEach ($dbmem in $dbmembers) - { - [PSCustomObject]@{ - SqlInstance = $instance - Database = $db.name - Role = $dbrole.name - Member = $dbmem.tostring() - } - } - } - } - } - } + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('SqlServer', 'ServerInstance')] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeServerLevel, + [switch]$NoFixedRole + ) + + process { + + foreach ($instance in $sqlinstance) { + Write-Verbose "Connecting to $Instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Warning "Failed to connect to $instance" + continue + } + + if ($IncludeServerLevel) { + Write-Verbose "Server Role Members included" + $instroles = $null + Write-Verbose "Getting Server Roles on $instance" + $instroles = $server.roles + if ($NoFixedRole) { + $instroles = $instroles | Where-Object { $_.isfixedrole -eq $false } + } + ForEach ($instrole in $instroles) { + Write-Verbose "Getting Server Role Members for $instrole on $instance" + $irmembers = $null + $irmembers = $instrole.enumserverrolemembers() + ForEach ($irmem in $irmembers) { + [PSCustomObject]@{ + SQLInstance = $instance + Database = $null + Role = $instrole.name + Member = $irmem.tostring() + } + } + } + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Verbose "Checking accessibility of $db on $instance" + + if ($db.IsAccessible -ne $true) { + Write-Warning "Database $db on $instance is not accessible" + continue + } + + $dbroles = $db.roles + Write-Verbose "Getting Database Roles for $db on $instance" + + if ($NoFixedRole) { + $dbroles = $dbroles | Where-Object { $_.isfixedrole -eq $false } + } + + foreach ($dbrole in $dbroles) { + Write-Verbose "Getting Database Role Members for $dbrole in $db on $instance" + $dbmembers = $dbrole.enummembers() + ForEach ($dbmem in $dbmembers) { + [PSCustomObject]@{ + SqlInstance = $instance + Database = $db.name + Role = $dbrole.name + Member = $dbmem.tostring() + } + } + } + } + } + } } diff --git a/functions/Get-DbaRunningJob.ps1 b/functions/Get-DbaRunningJob.ps1 index ac728c550b03..bea639c1db43 100644 --- a/functions/Get-DbaRunningJob.ps1 +++ b/functions/Get-DbaRunningJob.ps1 @@ -1,108 +1,88 @@ -FUNCTION Get-DbaRunningJob -{ -<# -.SYNOPSIS -Returns all non idle agent jobs running on the server. +function Get-DbaRunningJob { + <# + .SYNOPSIS + Returns all non-idle Agent jobs running on the server. -.DESCRIPTION -This function returns agent jobs that active on the SQL Server intance when calling the command. The information is gathered the SMO JobServer.jobs and be returned either in detailed or standard format + .DESCRIPTION + This function returns agent jobs that active on the SQL Server instance when calling the command. The information is gathered the SMO JobServer.jobs and be returned either in detailed or standard format. -.PARAMETER SqlServer -SQLServer name or SMO object representing the SQL Server to connect to. This can be a -collection and recieve pipeline input + .PARAMETER SqlInstance + The SQL Server instance to connect to. -.PARAMETER SqlCredential -PSCredential object to connect as. If not specified, currend Windows login will be used. + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) -.PARAMETER Detailed -Returns more information about the running jobs than standard + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -.NOTES -Original Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . -You should have received a copy of the GNU General Public License -along with this program. If not, see . + .NOTES + Tags: Process, Session, ActivityMonitor, Agent, Job + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT -.LINK -https://dbatools.io/Get-DbaRunningJob + .LINK + https://dbatools.io/Get-DbaRunningJob -.EXAMPLE -Get-DbaRunningJob -SqlServer localhost -Returns any active jobs on the localhost + .EXAMPLE + Get-DbaRunningJob -SqlInstance localhost -.EXAMPLE -Get-DbaRunningJob -SqlServer localhost -Detailed -Returns a detailed output of any active jobs on the localhost + Returns any active jobs on localhost. -.EXAMPLE -'localhost','localhost\namedinstance' | Get-DbaRunningJob -Returns all active jobs on multiple instances piped into the function + .EXAMPLE + Get-DbaRunningJob -SqlInstance localhost -Detailed -#> - [CmdletBinding()] - Param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] - [Alias("ServerInstance", "SqlInstance", "SqlServers")] - [string[]]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential, - [switch]$Detailed - ) - BEGIN - { - $output = @() - } - PROCESS - { - FOREACH ($Server in $SqlServer) - { - TRY - { - $server = Connect-SqlServer -SqlServer $server -SqlCredential $sqlcredential - } - CATCH - { - Write-Verbose "Failed to connect to: $Server" - continue - } - - $jobs = $server.JobServer.jobs | Where-Object { $_.CurrentRunStatus -ne 'Idle' } - - IF (!$jobs) - { - Write-Verbose "No Jobs are currently running on: $Server" - } - ELSE - { - foreach ($job in $jobs) - { - $output += [pscustomobject]@{ - ServerName = $Server.Name - Name = $job.name - Category = $job.Category - CurrentRunStatus = $job.CurrentRunStatus - CurrentRunStep = $job.CurrentRunStep - HasSchedule = $job.HasSchedule - LastRunDate = $job.LastRunDate - LastRunOutcome = $job.LastRunOutcome - JobStep = $job.JobSteps - } - } - } - } - } - END - { - IF ($Detailed -eq $true) - { - return $output | Sort-Object ServerName - } - ELSE - { - return ($output | Sort-Object ServerName | Select-Object ServerName, Name, Category, CurrentRunStatus, CurrentRunStep) - } - } -} + Returns a detailed output of any active jobs on localhost. + + .EXAMPLE + 'localhost','localhost\namedinstance' | Get-DbaRunningJob + + Returns all active jobs on multiple instances piped into the function. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failed to connect to: $Server." -Target $server -ErrorRecord $_ -Continue + } + + $jobs = $server.JobServer.jobs | Where-Object { $_.CurrentRunStatus -ne 'Idle' } + + if (!$jobs) { + Write-Message -Level Verbose -Message "No Jobs are currently running on: $Server." + } + else { + foreach ($job in $jobs) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Name = $job.name + Category = $job.Category + CurrentRunStatus = $job.CurrentRunStatus + CurrentRunStep = $job.CurrentRunStep + HasSchedule = $job.HasSchedule + LastRunDate = $job.LastRunDate + LastRunOutcome = $job.LastRunOutcome + JobStep = $job.JobSteps + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSchemaChangeHistory.ps1 b/functions/Get-DbaSchemaChangeHistory.ps1 new file mode 100644 index 000000000000..d85b1b9199ed --- /dev/null +++ b/functions/Get-DbaSchemaChangeHistory.ps1 @@ -0,0 +1,152 @@ +function Get-DbaSchemaChangeHistory { + <# + .SYNOPSIS + Gets DDL changes logged in the system trace. + + .DESCRIPTION + Queries the default system trace for any DDL changes in the specified timeframe + Only works with SQL 2005 and later, as the system trace didn't exist before then + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Since + A date from which DDL changes should be returned. Default is to start at the beggining of the current trace file + + .PARAMETER Object + The name of a SQL Server object you want to look for changes on + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Migration, Backup, Database + Author: Stuart Moore (@napalmgram - http://stuart-moore.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSchemaChangeHistory + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost + + Returns all DDL changes made in all databases on the SQL Server instance localhost since the system trace began + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made in all databases on the SQL Server instance localhost in the last 7 days + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Database Finance, Prod -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made in the Prod and Finance databases on the SQL Server instance localhost in the last 7 days + + .EXAMPLE + Get-DbaSchemaChangeHistory -SqlInstance localhost -Database Finance -Object AccountsTable -Since (Get-Date).AddDays(-7) + + Returns all DDL changes made to the AccountsTable object in the Finance database on the SQL Server instance localhost in the last 7 days + + #> + + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [DbaDateTime]$Since, + [string[]]$Object, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($Server.Version.Major -le 8) { + Stop-Function -Message "This command doesn't support SQL Server 2000, sorry about that" + return + } + $TraceFileQuery = "select path from sys.traces where is_default = 1" + + $TraceFile = $server.Query($TraceFileQuery) | Select-Object Path + + $Databases = $server.Databases + + if ($Database) { $Databases = $Databases | Where-Object Name -in $database } + + if ($ExcludeDatabase) { $Databases = $Databases | Where-Object Name -notin $ExcludeDatabase } + + foreach ($db in $Databases) { + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "$db on $server is inaccessible" -Continue + } + + $sql = "select SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + tt.databasename as 'DatabaseName', + starttime as 'DateModified', + Sessionloginname as 'LoginName', + NTusername as 'UserName', + applicationname as 'ApplicationName', + case eventclass + When '46' Then 'Create' + when '47' Then 'Drop' + when '164' then 'Alter' + end as 'DDLOperation', + s.name+'.'+o.name as 'Object', + o.type_desc as 'ObjectType' + from + sys.objects o inner join + sys.schemas s on s.schema_id=o.schema_id + cross apply (select * from ::fn_trace_gettable('$($TraceFile.path)',default) where ObjectID=o.object_id ) tt + where tt.objecttype not in (21587) + and tt.DatabaseID=db_id() + and tt.EventSubClass=0" + + if ($null -ne $since) { + $sql = $sql + " and tt.StartTime>'$Since' " + } + if ($null -ne $object) { + $sql = $sql + " and o.name in ('$($object -join ''',''')') " + } + + $sql = $sql + " order by tt.StartTime asc" + Write-Message -Level Verbose -Message "Querying Database $db on $instance" + Write-Message -Level Debug -Message "SQL: $sql" + + $db.Query($sql) | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, DatabaseName, DateModified, LoginName, UserName, ApplicationName, DDLOperation, Object, ObjectType + } + } + } +} + diff --git a/functions/Get-DbaServerAudit.ps1 b/functions/Get-DbaServerAudit.ps1 new file mode 100644 index 000000000000..f1f1122367af --- /dev/null +++ b/functions/Get-DbaServerAudit.ps1 @@ -0,0 +1,96 @@ +function Get-DbaServerAudit { + <# + .SYNOPSIS + Gets SQL Security Audit information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaServerAudit command gets SQL Security Audit information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Audit + Return only specific audits + + .PARAMETER ExcludeAudit + Exclude specific audits + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Audit, Security, SqlAudit + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerAudit + + .EXAMPLE + Get-DbaServerAudit -SqlInstance localhost + + Returns all Security Audits on the local default SQL Server instance + + .EXAMPLE + Get-DbaServerAudit -SqlInstance localhost, sql2016 + + Returns all Security Audits for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [string[]]$Audit, + [string[]]$ExcludeAudit, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $audits = $server.Audits + + if (Test-Bound -ParameterName Audit) { + $audits = $audits | Where-Object Name -in $Audit + } + if (Test-Bound -ParameterName ExcludeAudit) { + $audits = $audits | Where-Object Name -notin $ExcludeAudit + } + + foreach ($currentaudit in $audits) { + $directory = $currentaudit.FilePath.TrimEnd("\") + $filename = $currentaudit.FileName + $fullname = "$directory\$filename" + $remote = $fullname.Replace(":", "$") + $remote = "\\$($currentaudit.Parent.NetName)\$remote" + + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name ComputerName -value $currentaudit.Parent.NetName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name InstanceName -value $currentaudit.Parent.ServiceName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name SqlInstance -value $currentaudit.Parent.DomainInstanceName + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name FullName -value $fullname + Add-Member -Force -InputObject $currentaudit -MemberType NoteProperty -Name RemoteFullName -value $remote + + Select-DefaultView -InputObject $currentaudit -Property ComputerName, InstanceName, SqlInstance, Name, 'Enabled as IsEnabled', FullName + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaServerAuditSpecification.ps1 b/functions/Get-DbaServerAuditSpecification.ps1 new file mode 100644 index 000000000000..72addceb8b34 --- /dev/null +++ b/functions/Get-DbaServerAuditSpecification.ps1 @@ -0,0 +1,78 @@ +function Get-DbaServerAuditSpecification { + <# + .SYNOPSIS + Gets SQL Security Audit Specification information for each instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaServerAuditSpecification command gets SQL Security Audit Specification information for each instance(s) of SQL Server. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Audit, Security, SqlAudit + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerAuditSpecification + + .EXAMPLE + Get-DbaServerAuditSpecification -SqlInstance localhost + + Returns all Security Audit Specifications on the local default SQL Server instance + + .EXAMPLE + Get-DbaServerAuditSpecification -SqlInstance localhost, sql2016 + + Returns all Security Audit Specifications for the local and sql2016 SQL Server instances + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Verbose "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.versionMajor -lt 10) { + Write-Warning "Server Audits are only supported in SQL Server 2008 and above. Quitting." + continue + } + + foreach ($auditSpecification in $server.ServerAuditSpecifications) { + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $auditSpecification -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $auditSpecification -Property ComputerName, InstanceName, SqlInstance, ID, Name, AuditName, Enabled, CreateDate, DateLastModified, Guid + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlServerAuditSpecification + } +} diff --git a/functions/Get-DbaServerInstallDate.ps1 b/functions/Get-DbaServerInstallDate.ps1 new file mode 100644 index 000000000000..014424904db2 --- /dev/null +++ b/functions/Get-DbaServerInstallDate.ps1 @@ -0,0 +1,129 @@ +function Get-DbaServerInstallDate { + <# +.SYNOPSIS +Returns the install date of a SQL Instance and Windows Server, depending on what is passed. + +.DESCRIPTION +By default, this command returns for each SQL Instance instance passed in: +SQL Instance install date, formatted as a string +Hosting Windows server install date, formatted as a string + +.PARAMETER SqlInstance +The SQL Server that you're connecting to. + +.PARAMETER SqlCredential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER Credential +Credential object used to connect to the SQL Server as a different user + +.PARAMETER IncludeWindows +Includes the Windows Server Install date information + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: CIM +Author: Mitchell Hamann (@SirCaptainMitch), mitchellhamann.com + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaServerInstallDate + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance SqlBox1\Instance2 + +Returns an object with SQL Instance Install date as a string and the Windows install date as string. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance winserver\sqlexpress, sql2016 + +Returns an object with SQL Instance Install date as a string and the Windows install date as a string for both SQLInstances that are passed to the cmdlet. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance sqlserver2014a, sql2016 + +Returns an object with only the SQL Server Install date as a string. + +.EXAMPLE +Get-DbaServerInstallDate -SqlInstance sqlserver2014a, sql2016 -IncludeWindows + +Returns an object with the Windows Install date and the SQL install date as a string. + +.EXAMPLE +Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaServerInstallDate + +Returns an object with SQL Instance install date as a string for every server listed in the Central Management Server on sql2014 + +#> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [PSCredential] + $Credential, + [Switch]$IncludeWindows, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -ge 9) { + Write-Message -Level Verbose -Message "Getting Install Date for: $instance" + $sql = "SELECT create_date FROM sys.server_principals WHERE sid = 0x010100000000000512000000" + [DbaDateTime]$sqlInstallDate = $server.Query($sql, 'master', $true).create_date + + } + else { + Write-Message -Level Verbose -Message "Getting Install Date for: $instance" + $sql = "SELECT schemadate FROM sysservers" + [DbaDateTime]$sqlInstallDate = $server.Query($sql, 'master', $true).create_date + } + + $WindowsServerName = $server.ComputerNamePhysicalNetBIOS + + if ($IncludeWindows) { + try { + [DbaDateTime]$windowsInstallDate = (Get-DbaCmObject -ClassName win32_OperatingSystem -ComputerName $WindowsServerName -Credential $Credential -EnableException).InstallDate + } + catch { + Stop-Function -Message "Failed to connect to: $WindowsServerName" -Continue -Target $instance -ErrorRecord $_ + } + } + + $object = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + SqlInstallDate = $sqlInstallDate + WindowsInstallDate = $windowsInstallDate + } + + if ($IncludeWindows) { + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, SqlInstallDate, WindowsInstallDate + } + else { + Select-DefaultView -InputObject $object -Property ComputerName, InstanceName, SqlInstance, SqlInstallDate + } + + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaServerProtocol.ps1 b/functions/Get-DbaServerProtocol.ps1 index 24ef4af8025b..1a67199367fb 100644 --- a/functions/Get-DbaServerProtocol.ps1 +++ b/functions/Get-DbaServerProtocol.ps1 @@ -1,8 +1,7 @@ -Function Get-DbaServerProtocol -{ -<# +function Get-DbaServerProtocol { + <# .SYNOPSIS - Gets the SQL Server related server protocols on a computer. + Gets the SQL Server related server protocols on a computer. .DESCRIPTION Gets the SQL Server related server protocols on one or more computers. @@ -16,14 +15,17 @@ Function Get-DbaServerProtocol .PARAMETER Credential Credential object used to connect to the computer as a different user. + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + .NOTES Author: Klaas Vandenberghe ( @PowerDBAKlaas ) Tags: Protocol dbatools PowerShell module (https://dbatools.io) Copyright (C) 2016 Chrissy LeMaire - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. + License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Get-DbaServerProtocol @@ -33,7 +35,7 @@ Function Get-DbaServerProtocol Gets the SQL Server related server protocols on computer sqlserver2014a. - .EXAMPLE + .EXAMPLE 'sql1','sql2','sql3' | Get-DbaServerProtocol Gets the SQL Server related server protocols on computers sql1, sql2 and sql3. @@ -47,92 +49,47 @@ Function Get-DbaServerProtocol (Get-DbaServerProtocol -ComputerName sql1 | Where { $_.DisplayName = 'via' }).Disable() Disables the VIA ServerNetworkProtocol on computer sql1. - If succesfull, returncode 0 is shown. + If successful, returncode 0 is shown. #> -[CmdletBinding()] -Param ( - [parameter(ValueFromPipeline)] - [Alias("cn","host","Server")] - [string[]]$ComputerName = $env:COMPUTERNAME, - [PSCredential] [System.Management.Automation.CredentialAttribute()]$Credential -) - -BEGIN - { - $FunctionName = (Get-PSCallstack)[0].Command - $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique - } -PROCESS - { - foreach ( $Computer in $ComputerName ) - { - $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential - if ( $Server.ComputerName ) - { - $Computer = $server.ComputerName - Write-Verbose "$FunctionName - Getting SQL Server namespace on $Computer via CIM (WSMan)" - $namespace = Get-CimInstance -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -ClassName "__NAMESPACE" -Filter "Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | - Where-Object {(Get-CimInstance -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue).count -gt 0} | - Sort-Object Name -Descending | Select-Object -First 1 - if ( $namespace.Name ) - { - Write-Verbose "$FunctionName - Getting Cim class ServerNetworkProtocol in Namespace $($namespace.Name) on $Computer via CIM (WSMan)" - try - { - $prot = Get-CimInstance -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue - $prot | Add-Member -MemberType ScriptMethod -Name Enable -Value {Invoke-CimMethod -MethodName SetEnable -InputObject $this } - $prot | Add-Member -MemberType ScriptMethod -Name Disable -Value {Invoke-CimMethod -MethodName SetDisable -InputObject $this } - foreach ( $protocol in $prot ) { Select-DefaultView -InputObject $protocol -Property 'PSComputerName as ComputerName', 'InstanceName', 'ProtocolDisplayName as DisplayName', 'ProtocolName as Name', 'MultiIpconfigurationSupport as MultiIP', 'Enabled as IsEnabled' } - } - catch - { - Write-Warning "$FunctionName - No Sql ServerNetworkProtocol found on $Computer via CIM (WSMan)" - } - } #if namespace WSMan - else - { - Write-Verbose "$FunctionName - Getting computer information from $Computer via CIMsession (DCOM)" - $sessionoption = New-CimSessionOption -Protocol DCOM - $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential - if ( $CIMSession ) - { - Write-Verbose "$FunctionName - Get ComputerManagement Namespace in CIMsession on $Computer with protocol DCom." - $namespace = Get-CimInstance -CimSession $CIMsession -NameSpace root\Microsoft\SQLServer -ClassName "__NAMESPACE" -Filter "Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | - Where-Object {(Get-CimInstance -CimSession $CIMsession -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue).count -gt 0} | - Sort-Object Name -Descending | Select-Object -First 1 - } #if CIMsession DCom - else - { - Write-Warning "$FunctionName - can't create CIMsession via DCom on $Computer" - continue - } #else no CIMsession DCom - if ( $namespace.Name ) - { - Write-Verbose "$FunctionName - Getting Cim class ServerNetworkProtocol in Namespace $($namespace.Name) on $Computer via CIM (DCOM)" - try - { - $prot = Get-CimInstance -CimSession $CIMsession -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue - $prot | Add-Member -MemberType ScriptMethod -Name Enable -Value {Invoke-CimMethod -MethodName SetEnable -InputObject $this } - $prot | Add-Member -MemberType ScriptMethod -Name Disable -Value {Invoke-CimMethod -MethodName SetDisable -InputObject $this } - foreach ( $protocol in $prot ) { Select-DefaultView -InputObject $protocol -Property 'PSComputerName as ComputerName', 'InstanceName', 'ProtocolDisplayName as DisplayName', 'ProtocolName as Name', 'MultiIpconfigurationSupport as MultiIP', 'Enabled as IsEnabled' } + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($Computer in $ComputerName.ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ($Server.FullComputerName) { + $Computer = $server.FullComputerName + Write-Message -Level Verbose -Message "Getting SQL Server namespace on $computer" + $namespace = Get-DbaCmObject -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -Query "Select * FROM __NAMESPACE WHERE Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | + Where-Object { (Get-DbaCmObject -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue).count -gt 0 } | + Sort-Object Name -Descending | Select-Object -First 1 + if ($namespace.Name) { + Write-Message -Level Verbose -Message "Getting Cim class ServerNetworkProtocol in Namespace $($namespace.Name) on $Computer" + try { + $prot = Get-DbaCmObject -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -ClassName ServerNetworkProtocol -ErrorAction SilentlyContinue + $prot | Add-Member -Force -MemberType ScriptMethod -Name Enable -Value { Invoke-CimMethod -MethodName SetEnable -InputObject $this } + $prot | Add-Member -Force -MemberType ScriptMethod -Name Disable -Value { Invoke-CimMethod -MethodName SetDisable -InputObject $this } + foreach ($protocol in $prot) { Select-DefaultView -InputObject $protocol -Property 'PSComputerName as ComputerName', 'InstanceName', 'ProtocolDisplayName as DisplayName', 'ProtocolName as Name', 'MultiIpconfigurationSupport as MultiIP', 'Enabled as IsEnabled' } } - catch - { - Write-Warning "$FunctionName - No Sql ServerNetworkProtocol found on $Computer via CIM (DCOM)" + catch { + Write-Message -Level Warning -Message "No Sql ServerNetworkProtocol found on $Computer" } - if ( $CIMsession ) { Remove-CimSession $CIMsession } - } #if namespace DCom - else - { - Write-Warning "$FunctionName - No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." - } #else no namespace DCom - } #else no namespace WSMan - } #if computername - else - { - Write-Warning "$FunctionName - Failed to connect to $Computer" - } - } #foreach computer - } #PROCESS -} #function + } + else { + Write-Message -Level Warning -Message "No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." + } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + } +} diff --git a/functions/Get-DbaServerRole.ps1 b/functions/Get-DbaServerRole.ps1 new file mode 100644 index 000000000000..3709b665963a --- /dev/null +++ b/functions/Get-DbaServerRole.ps1 @@ -0,0 +1,94 @@ +function Get-DbaServerRole { + <# + .SYNOPSIS + Gets the list of server-level roles. + + .DESCRIPTION + Gets the list of server-level roles for SQL Server instance. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER ServerRole + Server-Level role to filter results to that role only. + + .PARAMETER ExcludeServerRole + Server-Level role to exclude from results. + + .PARAMETER ExcludeFixedRole + Filter the fixed server-level roles. Only applies to SQL Server 2017 that supports creation of server-level roles. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ServerRole, Security + Original Author: Shawn Melton (@wsmelton) + + Website: https: //dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaServerRole + + .EXAMPLE + Get-DbaServerRole -SqlInstance sql2016a + + Outputs list of server-level roles for sql2016a instance. + + .EXAMPLE + Get-DbaServerRole -SqlInstance sql2017a -ExcludeFixedRole + + Outputs the server-level role(s) that are not fixed roles on sql2017a instance. + #> + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$ServerRole, + [object[]]$ExcludeServerRole, + [switch]$ExcludeFixedRole, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $roles = $server.Roles + + if ($ServerRole) { + $roles = $roles | Where-Object Name -In $ServerRole + } + if ($ExcludeServerRole) { + $roles = $roles | Where-Object Name -NotIn $ExcludeServerRole + } + if ($ExcludeFixedRole) { + $roles = $roles | Where-Object IsFixedRole -eq $false + } + + foreach ($role in $roles) { + $members = $role.EnumMemberNames() + + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name Login -Value $members + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $role -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + $default = 'ComputerName', 'InstanceName', 'SqlInstance', 'Name as Role', 'IsFixedRole', 'DateCreated', 'DateModified' + Select-DefaultView -InputObject $role -Property $default + } + } + } +} diff --git a/functions/Get-DbaSpConfigure.ps1 b/functions/Get-DbaSpConfigure.ps1 index 1ba177f6e8d8..97b4e1d392b6 100644 --- a/functions/Get-DbaSpConfigure.ps1 +++ b/functions/Get-DbaSpConfigure.ps1 @@ -1,129 +1,124 @@ -FUNCTION Get-DbaSpConfigure -{ -<# -.SYNOPSIS -Returns all server level system configuration (sys.configuration/sp_configure) information - -.DESCRIPTION -This function returns server level system configuration (sys.configuration/sp_configure) information. The information is gathered through SMO Configuration.Properties. -The data includes the default value for each configuration, for quick identification of values that may have been changed. - -.PARAMETER SqlServer -SQLServer name or SMO object representing the SQL Server to connect to. This can be a -collection and recieve pipeline input - -.PARAMETER SqlCredential -PSCredential object to connect as. If not specified, current Windows login will be used. - -.PARAMETER Configs -Return only specific configurations -- auto-populated from source server - -.PARAMETER Detailed -Returns more information about the configuration settings than standard - -.NOTES -Original Author: Nic Cain, https://sirsql.net/ -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaSpConfigure - -.EXAMPLE -Get-DbaSpConfigure -SqlServer localhost -Returns server level configuration data on the localhost (ServerName, ConfigName, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) - -.EXAMPLE -'localhost','localhost\namedinstance' | Get-DbaSpConfigure -Returns system configuration information on multiple instances piped into the function - -.EXAMPLE -Get-DbaSpConfigure -SqlServer localhost -Returns server level configuration data on the localhost (ServerName, ConfigName, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) - -.EXAMPLE -Get-DbaSpConfigure -SqlServer sql2012 -Configs MaxServerMemory - -Returns only the system configuration for MaxServerMemory. Configs is autopopulated for tabbing convenience. - -#> - [CmdletBinding()] - Param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] - [Alias("ServerInstance", "SqlInstance", "SqlServers")] - [string[]]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential - ) - - DynamicParam { if ($SqlServer) { return (Get-ParamSqlServerConfigs -SqlServer $SqlServer -SqlCredential $SqlCredential) } } - - BEGIN - { - $configs = $psboundparameters.Configs - } - - PROCESS - { - FOREACH ($instance in $SqlServer) - { - TRY - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $sqlcredential - } - CATCH - { - Write-Warning "Failed to connect to: $instance" - continue - } - - #Get a list of the configuration property parents, and exlude the Parent, Properties values - $proplist = Get-Member -InputObject $server.Configuration -MemberType Property -Force | Select-Object Name | Where-Object { $_.Name -ne "Parent" -and $_.Name -ne "Properties" } - - if ($configs) - { - $proplist = $proplist | Where-Object { $_.Name -in $configs } - } - - #Grab the default sp_configure property values from the external function - $defaultConfigs = (Get-SqlDefaultSpConfigure -SqlVersion $server.VersionMajor).psobject.properties; - - #Iterate through the properties to get the configuration settings - foreach ($prop in $proplist) - { - $propInfo = $server.Configuration.$($prop.Name) - $defaultConfig = $defaultConfigs | Where-Object { $_.Name -eq $propInfo.DisplayName }; - - if ($defaultConfig.Value -eq $propInfo.RunValue) { $isDefault = $true } - else { $isDefault = $false } - - #Ignores properties that are not valid on this version of SQL - if (!([string]::IsNullOrEmpty($propInfo.RunValue))) - { - # some displaynames were empty - $displayname = $propInfo.DisplayName - if ($displayname.Length -eq 0) { $displayname = $prop.Name } - - [pscustomobject]@{ - ServerName = $server.Name - ConfigName = $prop.Name - DisplayName = $displayname - Description = $propInfo.Description - IsAdvanced = $propInfo.IsAdvanced - IsDynamic = $propInfo.IsDynamic - MinValue = $propInfo.Minimum - MaxValue = $propInfo.Maximum - ConfiguredValue = $propInfo.ConfigValue - RunningValue = $propInfo.RunValue - DefaultValue = $defaultConfig.Value - IsRunningDefaultValue = $isDefault - } - } - } - } - } -} +function Get-DbaSpConfigure { + <# + .SYNOPSIS + Returns all server level system configuration (sys.configuration/sp_configure) information + + .DESCRIPTION + This function returns server level system configuration (sys.configuration/sp_configure) information. The information is gathered through SMO Configuration.Properties. + The data includes the default value for each configuration, for quick identification of values that may have been changed. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a + collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + Return only specific configurations -- auto-populated from source server + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SpConfig, Configure, Configuration + Author: Nic Cain, https://sirsql.net/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSpConfigure + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance localhost + + Returns server level configuration data on the localhost (ServerName, Name, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) + + .EXAMPLE + 'localhost','localhost\namedinstance' | Get-DbaSpConfigure + + Returns system configuration information on multiple instances piped into the function + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance localhost + + Returns server level configuration data on the localhost (ServerName, Name, DisplayName, Description, IsAdvanced, IsDynamic, MinValue, MaxValue, ConfiguredValue, RunningValue, DefaultValue, IsRunningDefaultValue) + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance sql2012 -Name MaxServerMemory + + Returns only the system configuration for MaxServerMemory. Configs is auto-populated for tabbing convenience. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Config", "ConfigName")] + [string[]]$Name, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Warning "Failed to connect to: $instance" + continue + } + + #Get a list of the configuration property parents, and exclude the Parent, Properties values + $proplist = Get-Member -InputObject $server.Configuration -MemberType Property -Force | Select-Object Name | Where-Object { $_.Name -ne "Parent" -and $_.Name -ne "Properties" } + + if ($Name) { + $proplist = $proplist | Where-Object { $_.Name -in $Name } + } + + #Grab the default sp_configure property values from the external function + $defaultConfigs = (Get-SqlDefaultSpConfigure -SqlVersion $server.VersionMajor).psobject.properties; + + #Iterate through the properties to get the configuration settings + foreach ($prop in $proplist) { + $propInfo = $server.Configuration.$($prop.Name) + $defaultConfig = $defaultConfigs | Where-Object { $_.Name -eq $propInfo.DisplayName }; + + if ($defaultConfig.Value -eq $propInfo.RunValue) { $isDefault = $true } + else { $isDefault = $false } + + #Ignores properties that are not valid on this version of SQL + if (!([string]::IsNullOrEmpty($propInfo.RunValue))) { + # some displaynames were empty + $displayname = $propInfo.DisplayName + if ($displayname.Length -eq 0) { $displayname = $prop.Name } + + [pscustomobject]@{ + ServerName = $server.Name + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Name = $prop.Name + DisplayName = $displayname + Description = $propInfo.Description + IsAdvanced = $propInfo.IsAdvanced + IsDynamic = $propInfo.IsDynamic + MinValue = $propInfo.Minimum + MaxValue = $propInfo.Maximum + ConfiguredValue = $propInfo.ConfigValue + RunningValue = $propInfo.RunValue + DefaultValue = $defaultConfig.Value + IsRunningDefaultValue = $isDefault + Parent = $server + ConfigName = $prop.Name + } | Select-DefaultView -ExcludeProperty ServerName, Parent, ConfigName + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSpn.ps1 b/functions/Get-DbaSpn.ps1 index c621c7e07622..e573f935f652 100644 --- a/functions/Get-DbaSpn.ps1 +++ b/functions/Get-DbaSpn.ps1 @@ -1,197 +1,170 @@ -function Get-DbaSpn -{ -<# -.SYNOPSIS -Returns a list of set service principal names for a given computer/AD account +#ValidationTags#FlowControl,Pipeline# +function Get-DbaSpn { + <# + .SYNOPSIS + Returns a list of set service principal names for a given computer/AD account -.DESCRIPTION -Get a list of set SPNs. SPNs are set at the AD account level. You can either retrieve set SPNs for a computer, or any SPNs set for -a given active directry account. You can query one, or both. You'll get a list of every SPN found for either search term. + .DESCRIPTION + Get a list of set SPNs. SPNs are set at the AD account level. You can either retrieve set SPNs for a computer, or any SPNs set for + a given active directory account. You can query one, or both. You'll get a list of every SPN found for either search term. -.PARAMETER ComputerName -The servers you want to return set SPNs for. This is defaulted automatically to localhost. + .PARAMETER ComputerName + The servers you want to return set SPNs for. This is defaulted automatically to localhost. -.PARAMETER AccountName -The accounts you want to retrieve set SPNs for. + .PARAMETER AccountName + The accounts you want to retrieve set SPNs for. -.PARAMETER Credential -User credential to connect to the remote servers or active directory. This is a required parameter. - -.PARAMETER ByAccount -Shows all SPNs registered by the specified AccountName otherwise, only results will be shown for the specified ComputerName (which is localhost by default) + .PARAMETER Credential + User credential to connect to the remote servers or active directory. -.NOTES -Tags: SPN -Author: Drew Furgiuele (@pittfurg), http://www.port1433.com - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -.LINK -https://dbatools.io/Get-DbaSpn + .NOTES + Tags: SPN + Author: Drew Furgiuele (@pittfurg), http://www.port1433.com -.EXAMPLE -Get-DbaSpn -ServerName SQLSERVERA -Credential (Get-Credential) + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT -Returns a custom object with SearchTerm (ServerName) and the SPNs that were found + .LINK + https://dbatools.io/Get-DbaSpn -.EXAMPLE -Get-DbaSpn -AccountName doamain\account -Credential (Get-Credential) + .EXAMPLE + Get-DbaSpn -ServerName SQLSERVERA -Credential (Get-Credential) -Returns a custom object with SearchTerm (domain account) and the SPNs that were found + Returns a custom object with SearchTerm (ServerName) and the SPNs that were found -.EXAMPLE -Get-DbaSpn -ServerName SQLSERVERA,SQLSERVERB -Credential (Get-Credential) + .EXAMPLE + Get-DbaSpn -AccountName domain\account -Credential (Get-Credential) -Returns a custom object with SearchTerm (ServerName) and the SPNs that were found for multiple computers -#> + Returns a custom object with SearchTerm (domain account) and the SPNs that were found + + .EXAMPLE + Get-DbaSpn -ServerName SQLSERVERA,SQLSERVERB -Credential (Get-Credential) + + Returns a custom object with SearchTerm (ServerName) and the SPNs that were found for multiple computers + #> [cmdletbinding()] - param ( - [Parameter(Mandatory = $false,ValueFromPipeline = $true)] - [string[]]$ComputerName = $env:COMPUTERNAME, + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [string[]]$ComputerName, + [Parameter(Mandatory = $false)] + [string[]]$AccountName, [Parameter(Mandatory = $false)] - [string[]]$AccountName, - [switch]$ByAccount, - [Parameter(Mandatory = $false)] - [PSCredential]$Credential - ) - begin - { - Function Process-Account ($AccountName, $ByAccount) { - - ForEach ($account in $AccountName) - { - $ogaccount = $account - if ($account -like "*\*") - { - Write-Verbose "Account name ($account) provided in in domain\user format, stripping out domain info." - $account = ($account.split("\"))[1] - } - if ($account -like "*@*") - { - Write-Verbose "Account name ($account) provided in in user@domain format, stripping out domain info." - $account = ($account.split("@"))[0] - } - - $root = ([ADSI]"LDAP://RootDSE").defaultNamingContext - $adsearch = New-Object System.DirectoryServices.DirectorySearcher - - if ($Credential) - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root), $($Credential.UserName), $($Credential.GetNetworkCredential().password) - } - else - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root) - } - - $adsearch.SearchRoot = $domain - $adsearch.Filter = $("(&(samAccountName={0}))" -f $account) - - Write-Verbose "Looking for account $account..." - - try - { - $Result = $adsearch.FindOne() - } - catch - { - Write-Warning "AD lookup failure. This may be because the hostname ($computer) was not resolvable within the domain ($domain) or the SQL Server service account ($serviceaccount) couldn't be found in domain." - continue - } - - $properties = $result.Properties - - foreach ($spn in $result.Properties.serviceprincipalname) - { - if ($spn -match "\:") - { - try - { - $port = [int]($spn -Split "\:")[1] - } - catch - { - $port = $null - } - if ($spn -match "\/") - { - $serviceclass = ($spn -Split "\/")[0] - } - } - [pscustomobject] @{ - Input = $ogaccount - AccountName = $ogaccount - ServiceClass = "MSSQLSvc" # $serviceclass - Port = $port - SPN = $spn - } - } - } - continue - } - } - - process - { - foreach ($computer in $ComputerName) - { - if ($computer) - { - if ($computer.EndsWith('$')) - { - Write-Verbose "$computer is an account name. Processing as account." - Process-Account -AccountName $computer -ByAccount:$true - continue - } - } - - Write-Verbose "Getting SQL Server SPN for $computer" - $spns = Test-DbaSpn -ComputerName $computer -Credential $Credential - - $sqlspns = 0 - $spncount = $spns.count - Write-Verbose "Calculated $spncount SQL SPN entries that should exist for $computer" - foreach ($spn in $spns | Where-Object { $_.IsSet -eq $true }) - { - $sqlspns++ - - if ($accountName) - { - if ($accountName -eq $spn.InstanceServiceAccount) - { - [pscustomobject] @{ - Input = $computer - AccountName = $spn.InstanceServiceAccount - ServiceClass = "MSSQLSvc" - Port = $spn.Port - SPN = $spn.RequiredSPN - } - } - } - else - { - [pscustomobject] @{ - Input = $computer - AccountName = $spn.InstanceServiceAccount - ServiceClass = "MSSQLSvc" - Port = $spn.Port - SPN = $spn.RequiredSPN - } - } - } - Write-Verbose "Found $sqlspns set SQL SPN entries for $computer" - } - - if ($AccountName) - { - foreach ($account in $AccountName) - { - Process-Account -AccountName $account -ByAccount:$ByAccount - } - } - } + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Function Process-Account ($AccountName) { + + ForEach ($account in $AccountName) { + Write-Message -Message "Looking for account $account..." -Level Verbose + $searchfor = 'User' + if ($account.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $account -Type $searchfor -Credential $Credential -EnableException + } + catch { + Write-Message -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($Account)." -Level Warning + continue + } + if ($Result.Count -gt 0) { + try { + $results = $Result.GetUnderlyingObject() + $spns = $results.Properties.servicePrincipalName + } + catch { + Write-Message -Message "The SQL Service account ($Account) has been found, but you don't have enough permission to inspect its SPNs" -Level Warning + continue + } + } + else { + Write-Message -Message "The SQL Service account ($Account) has not been found" -Level Warning + continue + } + + foreach ($spn in $spns) { + if ($spn -match "\:") { + try { + $port = [int]($spn -Split "\:")[1] + } + catch { + $port = $null + } + if ($spn -match "\/") { + $serviceclass = ($spn -Split "\/")[0] + } + } + [pscustomobject] @{ + Input = $Account + AccountName = $Account + ServiceClass = "MSSQLSvc" # $serviceclass + Port = $port + SPN = $spn + } + } + } + } + if ($ComputerName.Count -eq 0 -and $AccountName.Count -eq 0) { + $ComputerName = @($env:COMPUTERNAME) + } + } + + process { + + foreach ($computer in $ComputerName) { + if ($computer) { + if ($computer.EndsWith('$')) { + Write-Message -Message "$computer is an account name. Processing as account." -Level Verbose + Process-Account -AccountName $computer + continue + } + } + + Write-Message -Message "Getting SQL Server SPN for $computer" -Level Verbose + $spns = Test-DbaSpn -ComputerName $computer -Credential $Credential + + $sqlspns = 0 + $spncount = $spns.count + Write-Message -Message "Calculated $spncount SQL SPN entries that should exist for $computer" -Level Verbose + foreach ($spn in $spns | Where-Object { $_.IsSet -eq $true }) { + $sqlspns++ + + if ($accountName) { + if ($accountName -eq $spn.InstanceServiceAccount) { + [pscustomobject] @{ + Input = $computer + AccountName = $spn.InstanceServiceAccount + ServiceClass = "MSSQLSvc" + Port = $spn.Port + SPN = $spn.RequiredSPN + } + } + } + else { + [pscustomobject] @{ + Input = $computer + AccountName = $spn.InstanceServiceAccount + ServiceClass = "MSSQLSvc" + Port = $spn.Port + SPN = $spn.RequiredSPN + } + } + } + Write-Message -Message "Found $sqlspns set SQL SPN entries for $computer" -Level Verbose + } + + if ($AccountName) { + foreach ($account in $AccountName) { + Process-Account -AccountName $account + } + } + } } diff --git a/functions/Get-DbaSqlBuildReference.ps1 b/functions/Get-DbaSqlBuildReference.ps1 new file mode 100644 index 000000000000..c8d8cce1cbd5 --- /dev/null +++ b/functions/Get-DbaSqlBuildReference.ps1 @@ -0,0 +1,302 @@ +function Get-DbaSqlBuildReference { + <# + .SYNOPSIS + Returns SQL Server Build infos on a SQL instance + + .DESCRIPTION + Returns info about the specific build of a SQL instance, including the SP, the CU and the reference KB, wherever possible. + It also includes End Of Support dates as specified on Microsoft Lifecycle Policy + + .PARAMETER Build + Instead of connecting to a real instance, pass a string identifying the build to get the info back. + + .PARAMETER SqlInstance + Target any number of instances, in order to return their build state. + + .PARAMETER SqlCredential + When connecting to an instance, use the credentials specified. + + .PARAMETER Update + Looks online for the most up to date reference, replacing the local one. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.00.4502" + + Returns information about a build identified by "12.00.4502" (which is SQL 2014 with SP1 and CU11) + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.00.4502" -Update + + Returns information about a build trying to fetch the most up to date index online. When the online version is newer, the local one gets overwritten + + .EXAMPLE + Get-DbaSqlBuildReference -Build "12.0.4502","10.50.4260" + + Returns information builds identified by these versions strings + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver2014a | Get-DbaSqlBuildReference + + Integrate with other commandlets to have builds checked for all your registered servers on sqlserver2014a + + .NOTES + Author: niphlod + Editor: Fred + Tags: SqlBuild + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlBuildReference +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding()] + Param ( + [version[]] + $Build, + + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance, + + [Alias("Credential")] + [PsCredential] + $SqlCredential, + + [switch] + $Update, + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + #region Helper functions + function Get-DbaSqlBuildReferenceIndex { + [CmdletBinding()] + Param ( + [string] + $Moduledirectory, + + [bool] + $Update, + + [bool] + $EnableException + ) + + $orig_idxfile = "$Moduledirectory\bin\dbatools-buildref-index.json" + $DbatoolsData = Get-DbaConfigValue -Name 'Path.DbatoolsData' + $writable_idxfile = Join-Path $DbatoolsData "dbatools-buildref-index.json" + + if (-not (Test-Path $orig_idxfile)) { + Write-Message -Level Warning -EnableException $EnableException -Message "Unable to read local SQL build reference file. Check your module integrity!" + } + + if ((-not (Test-Path $orig_idxfile)) -and (-not (Test-Path $writable_idxfile))) { + throw "Build reference file not found, check module health!" + } + + # If no writable copy exists, create one and return the module original + if (-not (Test-Path $writable_idxfile)) { + Copy-Item -Path $orig_idxfile -Destination $writable_idxfile -Force -ErrorAction Stop + $result = Get-Content $orig_idxfile -Raw | ConvertFrom-Json + } + + # Else, if both exist, update the writeable if necessary and return the current version + elseif (Test-Path $orig_idxfile) { + $module_content = Get-Content $orig_idxfile -Raw | ConvertFrom-Json + $data_content = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + + $module_time = Get-Date $module_content.LastUpdated + $data_time = Get-Date $data_content.LastUpdated + + $offline_time = $module_time + if ($module_time -gt $data_time) { + Copy-Item -Path $orig_idxfile -Destination $writable_idxfile -Force -ErrorAction Stop + $result = $module_content + } + else { + $result = $data_content + $offline_time = $data_time + } + # If Update is passed, try to fetch from online resource and store into the writeable + if ($Update) { + $WebContent = Get-DbaSqlBuildReferenceIndexOnline -EnableException $EnableException + if ($null -ne $WebContent) { + $webdata_content = $WebContent.Content | ConvertFrom-Json + $webdata_time = Get-Date $webdata_content.LastUpdated + if ($webdata_time -gt $offline_time) { + Write-Message -Level Output -Message "Index updated correctly, last update on: $(Get-Date -Date $webdata_time -Format s), was $(Get-Date -Date $offline_time -Format s)" + $WebContent.Content | Out-File $writable_idxfile -Encoding utf8 -ErrorAction Stop + $result = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + } + } + } + } + + # Else if the module version of the file no longer exists, but the writable version exists, return the writable version + else { + $result = Get-Content $writable_idxfile -Raw | ConvertFrom-Json + } + + $LastUpdated = Get-Date -Date $result.LastUpdated + if ($LastUpdated -lt (Get-Date).AddDays(-45)) { + Write-Message -Level Warning -EnableException $EnableException -Message "Index is stale, last update on: $(Get-Date -Date $LastUpdated -Format s), try the -Update parameter to fetch the most up to date index" + } + + $result.Data | Select-Object @{ Name = "VersionObject"; Expression = { [version]$_.Version } }, * + } + + function Get-DbaSqlBuildReferenceIndexOnline { + [CmdletBinding()] + Param ( + [bool] + $EnableException + ) + $url = Get-DbaConfigValue -Name 'assets.sqlbuildreference' + try { + $WebContent = Invoke-WebRequest $url -ErrorAction Stop + } + catch { + try { + Write-Message -Level Verbose -EnableException $EnableException -Message "Probably using a proxy for internet access, trying default proxy settings" + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + $WebContent = Invoke-WebRequest $url -ErrorAction Stop + } + catch { + Write-Message -Level Warning -EnableException $EnableException -Message "Couldn't download updated index from $url" + return + } + } + return $WebContent + } + + function Resolve-DbaSqlBuild { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [version] + $Build, + + $Data, + + [bool] + $EnableException + ) + + Write-Message -Level Verbose -EnableException $EnableException -Message "Looking for $Build" + + $IdxVersion = $Data | Where-Object Version -like "$($Build.Major).$($Build.Minor).*" + $Detected = @{ } + $Detected.MatchType = 'Approximate' + Write-Message -Level Verbose -EnableException $EnableException -Message "We have $($IdxVersion.Length) builds in store for this Release" + If ($IdxVersion.Length -eq 0) { + Write-Message -Level Warning -EnableException $EnableException -Message "No info in store for this Release" + $Detected.Warning = "No info in store for this Release" + } + else { + $LastVer = $IdxVersion[0] + } + foreach ($el in $IdxVersion) { + if ($null -ne $el.Name) { + $Detected.Name = $el.Name + } + if ($el.VersionObject -gt $Build) { + $Detected.MatchType = 'Approximate' + $Detected.Warning = "$Build not found, closest build we have is $($LastVer.Version)" + break + } + $LastVer = $el + if ($null -ne $el.SP) { + $Detected.SP = $el.SP + $Detected.CU = $null + } + if ($null -ne $el.CU) { + $Detected.CU = $el.CU + } + if ($null -ne $el.SupportedUntil) { + $Detected.SupportedUntil = (Get-Date -date $el.SupportedUntil) + } + $Detected.KB = $el.KBList + if ($el.Version -eq $Build) { + $Detected.MatchType = 'Exact' + break + } + } + return $Detected + } + #endregion Helper functions + + $moduledirectory = $MyInvocation.MyCommand.Module.ModuleBase + + try { + $IdxRef = Get-DbaSqlBuildReferenceIndex -Moduledirectory $moduledirectory -Update $Update -EnableException $EnableException + } + catch { + Stop-Function -Message "Error loading SQL build reference" -ErrorRecord $_ + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + #region Ensure the connection is established + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + + try { + $null = $server.Version.ToString() + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + #endregion Ensure the connection is established + + $Detected = Resolve-DbaSqlBuild -Build $server.Version -Data $IdxRef -EnableException $EnableException + + [PSCustomObject]@{ + SqlInstance = $server.DomainInstanceName + Build = $server.Version + NameLevel = $Detected.Name + SPLevel = $Detected.SP + CULevel = $Detected.CU + KBLevel = $Detected.KB + SupportedUntil = $Detected.SupportedUntil + MatchType = $Detected.MatchType + Warning = $Detected.Warning + } + } + + foreach ($buildstr in $Build) { + $Detected = Resolve-DbaSqlBuild -Build $buildstr -Data $IdxRef -EnableException $EnableException + + [PSCustomObject]@{ + SqlInstance = $null + Build = $buildstr + NameLevel = $Detected.Name + SPLevel = $Detected.SP + CULevel = $Detected.CU + KBLevel = $Detected.KB + SupportedUntil = $Detected.SupportedUntil + MatchType = $Detected.MatchType + Warning = $Detected.Warning + } | Select-DefaultView -ExcludeProperty SqlInstance + } + } +} diff --git a/functions/Get-DbaSqlFeature.ps1 b/functions/Get-DbaSqlFeature.ps1 new file mode 100644 index 000000000000..8dc42981dbf1 --- /dev/null +++ b/functions/Get-DbaSqlFeature.ps1 @@ -0,0 +1,116 @@ +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Get-DbaSqlFeature { + <# + .SYNOPSIS + Runs the SQL Server feature discovery report (setup.exe /Action=RunDiscovery) + + .DESCRIPTION + Runs the SQL Server feature discovery report (setup.exe /Action=RunDiscovery) + + Inspired by Dave Mason's (@BeginTry) post at + https://itsalljustelectrons.blogspot.be/2018/04/SQL-Server-Discovery-Report.html + + Assumptions: + 1. The sub-folder "Microsoft SQL Server" exists in $env:ProgramFiles, + even if SQL was installed to a non-default path. This has been + verified on SQL 2008R2 and SQL 2012. Further verification may be needed. + 2. The discovery report displays installed components for the version of SQL + Server associated with setup.exe, along with installed components of all + lesser versions of SQL Server that are installed. + + .PARAMETER ComputerName + The target computer. If the target is not localhost, it must have PowerShell remoting enabled. + + Note that this is not the SqlInstance, but rather the ComputerName + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Feature, Component + Author: Chrissy LeMaire (@cl) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlFeature + + .EXAMPLE + Get-DbaSqlFeature -ComputerName sql2017, sql2016, sql2005 + + Gets all SQL Server features for all instances on sql2017, sql2016 and sql2005. + + .EXAMPLE + Get-DbaSqlFeature -Verbose + + Gets all SQL Server features for all instances on localhost. Outputs to screen if no instances are found. + + .EXAMPLE + Get-DbaSqlFeature -ComputerName sql2017 -Credential (Get-Credential ad\sqladmin) + + Gets all SQL Server features for all instances on sql2017 using the ad\sqladmin credential (which has access to the Windows Server). + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [switch]$EnableException + ) + + begin { + $scriptblock = { + $setup = Get-ChildItem -Recurse -Include setup.exe -Path "$env:ProgramFiles\Microsoft SQL Server" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match 'Setup Bootstrap\\SQL' -or $_.FullName -match 'Bootstrap\\Release\\Setup.exe' -or $_.FullName -match 'Bootstrap\\Setup.exe' } | + Sort-Object FullName -Descending | Select-Object -First 1 + if ($setup) { + $null = Start-Process -FilePath $setup.FullName -ArgumentList "/Action=RunDiscovery /q" -Wait + $parent = Split-Path (Split-Path $setup.Fullname) + $xmlfile = Get-ChildItem -Recurse -Include SqlDiscoveryReport.xml -Path $parent | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + if ($xmlfile) { + $xml = [xml](Get-Content -Path $xmlfile) + $xml.ArrayOfDiscoveryInformation.DiscoveryInformation + } + } + } + } + + process { + foreach ($computer in $ComputerName) { + try { + $results = Invoke-Command2 -ComputerName $Computer -ScriptBlock $scriptblock -Credential $Credential -Raw + + if (-not $results) { + Write-Message -Level Verbose -Message "No features found on $computer" + } + + foreach ($result in $results) { + [pscustomobject]@{ + ComputerName = $computer + Product = $result.Product + Instance = $result.Instance + InstanceID = $result.InstanceID + Feature = $result.Feature + Language = $result.Language + Edition = $result.Edition + Version = $result.Version + Clustered = $result.Clustered + Configured = $result.Configured + } + } + } + catch { + Stop-Function -Continue -ErrorRecord $_ -Message "Failure" + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSqlInstanceProperty.ps1 b/functions/Get-DbaSqlInstanceProperty.ps1 new file mode 100644 index 000000000000..a2a608e2ed36 --- /dev/null +++ b/functions/Get-DbaSqlInstanceProperty.ps1 @@ -0,0 +1,155 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Get-DbaSqlInstanceProperty { + <# + .SYNOPSIS + Gets SQL Server instance properties of one or more instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaSqlInstanceProperty command gets SQL Server instance properties from the SMO object sqlserver. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER InstanceProperty + SQL Server instance property(ies) to include. + + .PARAMETER ExcludeInstanceProperty + SQL Server instance property(ies) to exclude. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, Configure, Configuration + Author: Klaas Vandenberghe (@powerdbaklaas) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlInstanceProperty + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance localhost + + Returns SQL Server instance properties on the local default SQL Server instance + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2, sql4\sqlexpress + + Returns SQL Server instance properties on default instance on sql2 and sqlexpress instance on sql4 + + .EXAMPLE + 'sql2','sql4' | Get-DbaSqlInstanceProperty + + Returns SQL Server instance properties on sql2 and sql4 + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2,sql4 -InstanceProperty DefaultFile + + Returns SQL Server instance property DefaultFile on instance sql2 and sql4 + + .EXAMPLE + Get-DbaSqlInstanceProperty -SqlInstance sql2,sql4 -ExcludeInstanceProperty DefaultFile + + Returns all SQL Server instance properties except DefaultFile on instance sql2 and sql4 + + .EXAMPLE + $cred = Get-Credential sqladmin + Get-DbaSqlInstanceProperty -SqlInstance sql2 -SqlCredential $cred + + Connects using sqladmin credential and returns SQL Server instance properties from sql2 + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$InstanceProperty, + [object[]]$ExcludeInstanceProperty, + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $infoProperties = $server.Information.Properties + + if ($InstanceProperty) { + $infoProperties = $infoProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $infoProperties = $infoProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $infoProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'Information' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering information properties for $instance." -Target $instance -ErrorRecord $_ -Continue + } + + try { + $userProperties = $server.UserOptions.Properties + + if ($InstanceProperty) { + $userProperties = $userProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $userProperties = $userProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $userProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'UserOption' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering user options for $instance." -Target $instance -ErrorRecord $_ -Continue + } + + try { + $settingProperties = $server.Settings.Properties + + if ($InstanceProperty) { + $settingProperties = $settingProperties | Where-Object Name -In $InstanceProperty + } + if ($ExcludeInstanceProperty) { + $settingProperties = $settingProperties | Where-Object Name -NotIn $ExcludeInstanceProperty + } + foreach ($prop in $settingProperties) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name PropertyType -Value 'Setting' + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value, PropertyType + } + } + catch { + Stop-Function -Message "Issue gathering settings for $instance." -Target $instance -ErrorRecord $_ -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSqlInstanceUserOption.ps1 b/functions/Get-DbaSqlInstanceUserOption.ps1 new file mode 100644 index 000000000000..7e1a1d3021c2 --- /dev/null +++ b/functions/Get-DbaSqlInstanceUserOption.ps1 @@ -0,0 +1,74 @@ +function Get-DbaSqlInstanceUserOption { + <# + .SYNOPSIS + Gets SQL Instance user options of one or more instance(s) of SQL Server. + + .DESCRIPTION + The Get-DbaSqlInstanceUserOption command gets SQL Instance user options from the SMO object sqlserver. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, Configure, UserOption + Author: Klaas Vandenberghe (@powerdbaklaas) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlInstanceUserOption + + .EXAMPLE + Get-DbaSqlInstanceUserOption -SqlInstance localhost + + Returns SQL Instance user options on the local default SQL Server instance + + .EXAMPLE + Get-DbaSqlInstanceUserOption -SqlInstance sql2, sql4\sqlexpress + + Returns SQL Instance user options on default instance on sql2 and sqlexpress instance on sql4 + + .EXAMPLE + 'sql2','sql4' | Get-DbaSqlInstanceUserOption + + Returns SQL Instance user options on sql2 and sql4 + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $props = $server.useroptions.properties + foreach ($prop in $props) { + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $prop -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Select-DefaultView -InputObject $prop -Property ComputerName, InstanceName, SqlInstance, Name, Value + } + } + } +} diff --git a/functions/Get-DbaSqlManagementObject.ps1 b/functions/Get-DbaSqlManagementObject.ps1 new file mode 100644 index 000000000000..eff3e82d9680 --- /dev/null +++ b/functions/Get-DbaSqlManagementObject.ps1 @@ -0,0 +1,148 @@ +function Get-DbaSqlManagementObject { + <# + .SYNOPSIS + Gets SQL Mangaement Object versions installed on the machine. + + .DESCRIPTION + The Get-DbaSqlManagementObject returns an object with the Version and the + Add-Type Load Template for each version on the server. + + .PARAMETER ComputerName + The name of the target you would like to check + + .PARAMETER Credential + This command uses Windows credentials. This parameter allows you to connect remotely as a different user. + + .PARAMETER VersionNumber + This is the specific version number you are looking for. The function will look + for that version only. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SMO + Author: Ben Miller (@DBAduck - http://dbaduck.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlManagementObject + + .EXAMPLE + Get-DbaSqlManagementObject + + Returns all versions of SMO on the computer + + .EXAMPLE + Get-DbaSqlManagementObject -VersionNumber 13 + + Returns just the version specified. If the version does not exist then it will return nothing. + + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] + $Credential, + [int]$VersionNumber, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (!$VersionNumber) { + $VersionNumber = 0 + } + $scriptblock = { + $VersionNumber = [int]$args[0] + + Write-Message -Level Verbose -Message "Checking currently loaded SMO version" + $loadedversion = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.Fullname -like "Microsoft.SqlServer.SMO,*" } + if ($loadedversion) { + $loadedversion = $loadedversion | ForEach-Object { + if ($_.Location -match "__") { + ((Split-Path (Split-Path $_.Location) -Leaf) -split "__")[0] + } + else { + ((Get-ChildItem -Path $_.Location).VersionInfo.ProductVersion) + } + } + } + + Write-Message -Level Verbose -Message "Looking for included smo library" + $localversion = [version](Get-ChildItem -Path "$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll").VersionInfo.ProductVersion + + foreach ($version in $localversion) { + if ($VersionNumber -eq 0) { + Write-Message -Level Verbose -Message "Did not pass a version" + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $localversion + Loaded = $loadedversion -contains $localversion + LoadTemplate = "Add-Type -Path $("$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll")" + } + } + else { + Write-Message -Level Verbose -Message "Passed version $VersionNumber, looking for that specific version" + if ($localversion.ToString().StartsWith("$VersionNumber.")) { + Write-Message -Level Verbose -Message "Found the Version $VersionNumber" + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $localversion + Loaded = $loadedversion -contains $localversion + LoadTemplate = "Add-Type -Path $("$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Smo.dll")" + } + } + } + } + + Write-Message -Level Verbose -Message "Looking for SMO in the Global Assembly Cache" + $smolist = (Get-ChildItem -Path "$env:SystemRoot\assembly\GAC_MSIL\Microsoft.SqlServer.Smo" | Sort-Object Name -Descending).Name + + foreach ($version in $smolist) { + $array = $version.Split("__") + if ($VersionNumber -eq 0) { + Write-Message -Level Verbose -Message "Did not pass a version, looking for all versions" + $currentversion = $array[0] + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $currentversion + Loaded = $loadedversion -contains $currentversion + LoadTemplate = "Add-Type -AssemblyName `"Microsoft.SqlServer.Smo, Version=$($array[0]), Culture=neutral, PublicKeyToken=89845dcd8080cc91`"" + } + } + else { + Write-Message -Level Verbose -Message "Passed version $VersionNumber, looking for that specific version" + if ($array[0].StartsWith("$VersionNumber.")) { + Write-Message -Level Verbose -Message "Found the Version $VersionNumber" + $currentversion = $array[0] + [PSCustomObject]@{ + ComputerName = $env:COMPUTERNAME + Version = $currentversion + Loaded = $loadedversion -contains $currentversion + LoadTemplate = "Add-Type -AssemblyName `"Microsoft.SqlServer.Smo, Version=$($array[0]), Culture=neutral, PublicKeyToken=89845dcd8080cc91`"" + } + } + } + } + } + } + + process { + foreach ($computer in $ComputerName.ComputerName) { + try { + Invoke-Command2 -ComputerName $computer -ScriptBlock $scriptblock -Credential $Credential -ArgumentList $VersionNumber -ErrorAction Stop + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $ComputerName + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSqlModule.ps1 b/functions/Get-DbaSqlModule.ps1 new file mode 100644 index 000000000000..8e127986854b --- /dev/null +++ b/functions/Get-DbaSqlModule.ps1 @@ -0,0 +1,174 @@ +function Get-DbaSqlModule { + <# + .SYNOPSIS + Displays all objects in sys.sys_modules after specified modification date. Works on SQL Server 2008 and above. + + .DESCRIPTION + Quickly find modules (Stored Procs, Functions, Views, Constraints, Rules, Triggers, etc) that have been modified in a database, or across all databases. + Results will exclude the module definition, but can be queried explicitly. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. + + .PARAMETER ModifiedSince + DateTime value to use as minimum modified date of module. + + .PARAMETER Type + Limit by specific type of module. Valid choices include: View, TableValuedFunction, DefaultConstraint, StoredProcedure, Rule, InlineTableValuedFunction, Trigger, ScalarFunction + + .PARAMETER NoSystemDb + Allows you to suppress output on system databases + + .PARAMETER NoSystemObjects + Allows you to suppress output on system objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Brandon Abshire, netnerds.net + Tags: StoredProcedure, Trigger + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlModule + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008, sqlserver2012 + Return all modules for servers sql2008 and sqlserver2012 sorted by Database, Modify_Date ASC. + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008, sqlserver2012 | Select * + Shows hidden definition column (informative wall of text). + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008 -Database TestDB -ModifiedSince "01/01/2017 10:00:00 AM" + Return all modules on server sql2008 for only the TestDB database with a modified date after 01/01/2017 10:00:00 AM. + + .EXAMPLE + Get-DbaSqlModule -SqlServer sql2008 -Type View, Trigger, ScalarFunction + Return all modules on server sql2008 for all databases that are triggers, views or scalar functions. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$ModifiedSince = "01/01/1900", + [ValidateSet("View", "TableValuedFunction", "DefaultConstraint", "StoredProcedure", "Rule", "InlineTableValuedFunction", "Trigger", "ScalarFunction")] + [string[]]$Type, + [switch]$NoSystemDb, + [switch]$NoSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $types = @() + + foreach ($t in $type) { + if ($t -eq "View") { $types += "VIEW" } + if ($t -eq "TableValuedFunction") { $types += "SQL_TABLE_VALUED_FUNCTION" } + if ($t -eq "DefaultConstraint") { $types += "DEFAULT_CONSTRAINT" } + if ($t -eq "StoredProcedure") { $types += "SQL_STORED_PROCEDURE" } + if ($t -eq "Rule") { $types += "RULE" } + if ($t -eq "InlineTableValuedFunction") { $types += "SQL_INLINE_TABLE_VALUED_FUNCTION" } + if ($t -eq "Trigger") { $types += "SQL_TRIGGER" } + if ($t -eq "ScalarFunction") { $types += "SQL_SCALAR_FUNCTION" } + } + + + $sql = "SELECT DB_NAME() AS DatabaseName, + so.name AS ModuleName, + so.object_id , + SCHEMA_NAME(so.schema_id) AS SchemaName , + so.parent_object_id , + so.type , + so.type_desc , + so.create_date , + so.modify_date , + so.is_ms_shipped , + sm.definition, + OBJECTPROPERTY(so.object_id, 'ExecIsStartUp') as startup + FROM sys.sql_modules sm + LEFT JOIN sys.objects so ON sm.object_id = so.object_id + WHERE so.modify_date >= '$($ModifiedSince)'" + if ($NoSystemObjects) { + $sql += "`n AND so.is_ms_shipped = 0" + } + if ($Type) { + $sqltypes = $types -join "','" + $sql += " AND type_desc in ('$sqltypes')" + } + $sql += "`n ORDER BY so.modify_date" + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = Get-DbaDatabase -SqlInstance $server + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + + foreach ($db in $databases) { + + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "The database $db is not accessible. Skipping database." -Target $db -Continue + } + + foreach ($row in $server.Query($sql, $db.name)) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DatabaseName + Name = $row.ModuleName + ObjectID = $row.object_id + SchemaName = $row.SchemaName + Type = $row.type_desc + CreateDate = $row.create_date + ModifyDate = $row.modify_date + IsMsShipped = $row.is_ms_shipped + ExecIsStartUp = $row.startup + Definition = $row.definition + } | Select-DefaultView -ExcludeProperty Definition + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSqlProductKey.ps1 b/functions/Get-DbaSqlProductKey.ps1 new file mode 100644 index 000000000000..f3a770100090 --- /dev/null +++ b/functions/Get-DbaSqlProductKey.ps1 @@ -0,0 +1,215 @@ +function Get-DbaSqlProductKey { + <# +.SYNOPSIS +Gets SQL Server Product Keys from local or destination SQL Servers. Works with SQL Server 2005-2016 + +.DESCRIPTION +Using a string of servers, a text file, or Central Management Server to provide a list of servers, this script will go to each server and get the product key for all installed instances. Clustered instances are supported as well. Requires regular user access to the SQL instances, SMO installed locally, Remote Registry enabled and accessible by the account running the script. + +Uses key decoder by Jakob Bindslet (http://goo.gl/1jiwcB) + +.PARAMETER SqlInstances +A comma separated list of servers. This can be the NetBIOS name, IP, or SQL instance name + +.PARAMETER SqlCms +Compiles list of servers to inventory using all servers stored within a Central Management Server. Requires having SQL Management Studio installed. + +.PARAMETER ServersFromFile +Uses a text file as input. The file must be formatted as such: +sqlserver1 +sqlserver2 + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.NOTES +Author: Chrissy LeMaire (@cl), netnerds.net +Tags: SQL, Product Key + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaSqlProductKey + +.EXAMPLE +Get-DbaSqlProductKey winxp, sqlservera, sqlserver2014a, win2k8 +Gets SQL Server versions, editions and product keys for all instances within each server or workstation. + +.EXAMPLE +Get-DbaSqlProductKey -SqlCms sqlserver01 +Gets SQL Server versions, editions and product keys for all instances within sqlserver01's Central Management Server + +.EXAMPLE +Get-DbaSqlProductKey -ServersFromFile C:\Scripts\servers.txt +Gets SQL Server versions, editions and product keys for all instances listed within C:\Scripts\servers.txt +#> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [string[]]$SqlInstances, + # Central Management Server + + [string]$SqlCms, + # File with one server per line + + [string]$ServersFromFile, + [PSCredential]$SqlCredential + ) + + BEGIN { + + Function Unlock-SqlInstanceKey { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [byte[]]$data, + [int]$version + ) + try { + if ($version -ge 11) { $binArray = ($data)[0..66] } + else { $binArray = ($data)[52..66] } + $charsArray = "B", "C", "D", "F", "G", "H", "J", "K", "M", "P", "Q", "R", "T", "V", "W", "X", "Y", "2", "3", "4", "6", "7", "8", "9" + for ($i = 24; $i -ge 0; $i--) { + $k = 0 + for ($j = 14; $j -ge 0; $j--) { + $k = $k * 256 -bxor $binArray[$j] + $binArray[$j] = [math]::truncate($k / 24) + $k = $k % 24 + } + $productKey = $charsArray[$k] + $productKey + if (($i % 5 -eq 0) -and ($i -ne 0)) { + $productKey = "-" + $productKey + } + } + } + catch { $productkey = "Cannot decode product key." } + return $productKey + } + } + + PROCESS { + + if ($SqlCms) { + if ($null -eq [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.RegisteredServers")) + { throw "Can't load CMS assemblies. You must have SQL Server Management Studio installed to use the -SqlCms switch." } + + Write-Verbose "Gathering SQL Servers names from Central Management Server" + $server = Connect-SqlInstance -SqlInstance $SqlCms -SqlCredential $SqlCredential + $sqlconnection = $server.ConnectionContext.SqlConnectionObject + + try { $cmstore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sqlconnection) } + catch { throw "Cannot access Central Management Server" } + $dbstore = $cmstore.DatabaseEngineServerGroup + $SqlInstances = $dbstore.GetDescendantRegisteredServers().servername + # Add the CM server itself, which can't be stored in the CM server. + $servers += $SqlCms + $basenames = @() + foreach ($server in $SqlInstances) { $basenames += $server.Split("\")[0] } + $SqlInstances = $basenames | Get-Unique + } + + If ($ServersFromFile) { + if ((Test-Path $ServersFromFile) -eq $false) { throw "Could not find file: $ServersFromFile" } + $SqlInstances = Get-Content $ServersFromFile + } + + if ([string]::IsNullOrEmpty($SqlInstances)) { $SqlInstances = $env:computername } + + $basepath = "SOFTWARE\Microsoft\Microsoft SQL Server" + # Loop through each server + $objectCollection = @() + foreach ($servername in $SqlInstances) { + $servername = $servername.Split("\")[0] + + if ($servername -eq "." -or $servername -eq "localhost" -or $servername -eq $env:computername) { + $localmachine = [Microsoft.Win32.RegistryHive]::LocalMachine + $defaultview = [Microsoft.Win32.RegistryView]::Default + $reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey($localmachine, $defaultview) + } + else { + # Get IP for remote registry access. It's the most reliable. + try { $ipaddr = ([System.Net.Dns]::GetHostAddresses($servername)).IPAddressToString } + catch { Write-Warning "Can't resolve $servername. Skipping."; continue } + + try { + $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $ipaddr) + } + catch { Write-Warning "Can't access registry for $servername. Is the Remote Registry service started?"; continue } + } + + $instances = $reg.OpenSubKey("$basepath\Instance Names\SQL", $false) + if ($instances -eq $null) { Write-Warning "No instances found on $servername. Skipping."; continue } + # Get Product Keys for all instances on the server. + foreach ($instance in $instances.GetValueNames()) { + if ($instance -eq "MSSQLSERVER") { $SqlInstance = $servername } + else { $SqlInstance = "$servername\$instance" } + + $subkeys = $reg.OpenSubKey("$basepath", $false) + $instancekey = $subkeys.GetSubKeynames() | Where-Object { $_ -like "*.$instance" } + if ($null -eq $instancekey) { $instancekey = $instance } # SQL 2k5 + + # Cluster instance hostnames are required for SMO connection + $cluster = $reg.OpenSubKey("$basepath\$instancekey\Cluster", $false) + if ($cluster -ne $null) { + $clustername = $cluster.GetValue("ClusterName") + if ($instance -eq "MSSQLSERVER") { $SqlInstance = $clustername } + else { $SqlInstance = "$clustername\$instance" } + } + + Write-Verbose "Attempting to connect to $SqlInstance" + try { $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential } + catch { Write-Warning "Can't connect to $SqlInstance or access denied. Skipping."; continue } + + $servicePack = $server.ProductLevel + Write-Debug "$servername $instance version is $($server.VersionMajor)" + switch ($server.VersionMajor) { + 9 { + $sqlversion = "SQL Server 2005 $servicePack" + $findkeys = $reg.OpenSubKey("$basepath\90\ProductID", $false) + foreach ($findkey in $findkeys.GetValueNames()) { + if ($findkey -like "DigitalProductID*") { $key = "$basepath\90\ProductID\$findkey" } + } + } + 10 { + $sqlversion = "SQL Server 2008 $servicePack" + $key = "$basepath\MSSQL10" + if ($server.VersionMinor -eq 50) { $key += "_50"; $sqlversion = "SQL Server 2008 R2 $servicePack" } + $key += ".$instance\Setup\DigitalProductID" + } + 11 { $key = "$basepath\110\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2012 $servicePack" } + 12 { $key = "$basepath\120\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2014 $servicePack" } + 13 { $key = "$basepath\130\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2016 $servicePack" } + default { Write-Warning "SQL version not currently supported."; continue } + } + if ($server.Edition -notlike "*Express*") { + try { + $subkey = Split-Path $key; $binaryvalue = Split-Path $key -leaf + $binarykey = $($reg.OpenSubKey($subkey)).GetValue($binaryvalue) + } + catch { $sqlkey = "Could not connect." } + try { $sqlkey = Unlock-SqlInstanceKey $binarykey $server.VersionMajor } + catch { } + } + else { $sqlkey = "SQL Server Express Edition" } + $server.ConnectionContext.Disconnect() + + $object = New-Object PSObject -Property @{ + "SQL Instance" = $SqlInstance + "SQL Version" = $sqlversion + "SQL Edition" = $server.Edition + "Product Key" = $sqlkey + } + $objectCollection += $object + } + $reg.Close() + } + $objectCollection | Select "SQL Instance", "SQL Version", "SQL Edition", "Product Key" + } + + END { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-SqlServerKey + } +} diff --git a/functions/Get-DbaSqlRegistryRoot.ps1 b/functions/Get-DbaSqlRegistryRoot.ps1 new file mode 100644 index 000000000000..db9ef22d5291 --- /dev/null +++ b/functions/Get-DbaSqlRegistryRoot.ps1 @@ -0,0 +1,99 @@ +function Get-DbaSqlRegistryRoot { + <# +.SYNOPSIS +Uses SQL WMI to find the Registry Root of each SQL Server instance on a computer + +.DESCRIPTION +Uses SQL WMI to find the Registry Root of each SQL Server instance on a computer + +.PARAMETER ComputerName +The target computer. This is not a SQL Server service, though if you pass a named SQL instance, it'll parse properly down to the computer name + +.PARAMETER Credential +Allows you to login to $ComputerName using alternative Windows credentials + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Configuration, Registry + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Get-DbaSqlRegistryRoot +Gets the registry root for all instances on localhost + +.EXAMPLE +Get-DbaSqlRegistryRoot -ComputerName server1 + +Gets the registry root for all instances on server1 + +#> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($computer in $computername) { + Write-Message -Level Verbose -Message "Connecting to SQL WMI on $($computer.ComputerName)" + try { + $sqlwmis = Invoke-ManagedComputerCommand -ComputerName $computer.ComputerName -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -match "SQL Server \(" + } + catch { + Stop-Function -Message $_ -Target $sqlwmi -Continue + } + + foreach ($sqlwmi in $sqlwmis) { + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Write-Message -Level Warning -Message "Can't find instance $vsname on $env:COMPUTERNAME" + return + } + } + + # vsname takes care of clusters + if ([System.String]::IsNullOrEmpty($vsname)) { + $vsname = $computer + if ($instancename -ne "MSSQLSERVER") { + $vsname = "$computer\$instancename" + } + } + + Write-Message -Level Verbose -Message "Regroot: $regroot" + Write-Message -Level Verbose -Message "InstanceName: $instancename" + Write-Message -Level Verbose -Message "VSNAME: $vsname" + + [pscustomobject]@{ + ComputerName = $computer.ComputerName + InstanceName = $instancename + SqlInstance = $vsname + Hive = "HKLM" + Path = $regroot + RegistryRoot = "HKLM:\$regroot" + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaSqlService.ps1 b/functions/Get-DbaSqlService.ps1 index 9d44ba902ee5..fcc2466b9c3d 100644 --- a/functions/Get-DbaSqlService.ps1 +++ b/functions/Get-DbaSqlService.ps1 @@ -1,157 +1,231 @@ -Function Get-DbaSqlService -{ -<# - .SYNOPSIS - Gets the SQL Server related services on a computer. - - .DESCRIPTION - Gets the SQL Server related services on one or more computers. - - Requires Local Admin rights on destination computer(s). - - .PARAMETER ComputerName - The SQL Server (or server in general) that you're connecting to. This command handles named instances. - - .PARAMETER Credential - Credential object used to connect to the computer as a different user. - - .PARAMETER Type - Use -Type to collect only services of the desired SqlServiceType. - Can be one of the following: "Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS" - - .NOTES - Author: Klaas Vandenberghe ( @PowerDBAKlaas ) - - dbatools PowerShell module (https://dbatools.io) - Copyright (C) 2016 Chrissy LeMaire - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - - .LINK - https://dbatools.io/Get-DbaSqlService - - .EXAMPLE - Get-DbaSqlService -ComputerName sqlserver2014a - - Gets the SQL Server related services on computer sqlserver2014a. - - .EXAMPLE - 'sql1','sql2','sql3' | Get-DbaSqlService - - Gets the SQL Server related services on computers sql1, sql2 and sql3. - - .EXAMPLE - Get-DbaSqlService -ComputerName sql1,sql2 | Out-Gridview - - Gets the SQL Server related services on computers sql1 and sql2, and shows them in a grid view. - - .EXAMPLE - Get-DbaSqlService -ComputerName $MyServers -Type SSRS +function Get-DbaSqlService { + <# + .SYNOPSIS + Gets the SQL Server related services on a computer. + + .DESCRIPTION + Gets the SQL Server related services on one or more computers. + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER InstanceName + Only returns services that belong to the specific instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER Type + Use -Type to collect only services of the desired SqlServiceType. + Can be one of the following: "Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS" + + .PARAMETER ServiceName + Can be used to specify service names explicitly, without looking for service types/instances. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Service, SqlServer, Instance, Connect + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Requires Local Admin rights on destination computer(s). + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSqlService + + .EXAMPLE + Get-DbaSqlService -ComputerName sqlserver2014a + + Gets the SQL Server related services on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3' | Get-DbaSqlService + + Gets the SQL Server related services on computers sql1, sql2 and sql3. + + .EXAMPLE + Get-DbaSqlService -ComputerName sql1,sql2 | Out-GridView + + Gets the SQL Server related services on computers sql1 and sql2, and shows them in a grid view. + + .EXAMPLE + Get-DbaSqlService -ComputerName $MyServers -Type SSRS + + Gets the SQL Server related services of type "SSRS" (Reporting Services) on computers in the variable MyServers. + + .EXAMPLE + $services = Get-DbaSqlService -ComputerName sql1 -Type Agent,Engine + $services.ChangeStartMode('Manual') + + Gets the SQL Server related services of types Sql Agent and DB Engine on computer sql1 and changes their startup mode to 'Manual'. + + .EXAMPLE + (Get-DbaSqlService sql1 -Type Engine).Restart($true) + + Calls a Restart method for each Engine service on computer sql1 with -Force option. + #> + [CmdletBinding(DefaultParameterSetName = "Search")] + Param ( + [parameter(ValueFromPipeline = $true, Position = 1)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [Parameter(ParameterSetName = "Search")] + [Alias("Instance")] + [string[]]$InstanceName, + [PSCredential]$Credential, + [Parameter(ParameterSetName = "Search")] + [ValidateSet("Agent", "Browser", "Engine", "FullText", "SSAS", "SSIS", "SSRS")] + [string[]]$Type, + [Parameter(ParameterSetName = "ServiceName")] + [string[]]$ServiceName, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + #Dictionary to transform service type IDs into the names from Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer.Services.Type + $ServiceIdMap = @( + @{ Name = "Engine"; Id = 1 }, + @{ Name = "Agent"; Id = 2 }, + @{ Name = "FullText"; Id = 3, 9 }, + @{ Name = "SSIS"; Id = 4 }, + @{ Name = "SSAS"; Id = 5 }, + @{ Name = "SSRS"; Id = 6 }, + @{ Name = "Browser"; Id = 7 }, + @{ Name = "Unknown"; Id = 8 } + ) + if ($PsCmdlet.ParameterSetName -match 'Search') { + if ($Type) { + $searchClause = "" + foreach ($itemType in $Type) { + foreach ($id in ($ServiceIdMap | Where-Object { $_.Name -eq $itemType }).Id) { + if ($searchClause) { $searchClause += ' OR ' } + $searchClause += "SQLServiceType = $id" + } + } + } + else { + $searchClause = "SQLServiceType > 0" + } + } + elseif ($PsCmdlet.ParameterSetName -match 'ServiceName') { + if ($ServiceName) { + $searchClause = "" + foreach ($sn in $ServiceName) { + if ($searchClause) { $searchClause += ' OR ' } + $searchClause += "ServiceName = '$sn'" + } + } + else { + $searchClause = "SQLServiceType > 0" + } + } + } + PROCESS { + foreach ($Computer in $ComputerName.ComputerName) { + $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential + if ($Server.FullComputerName) { + $Computer = $server.FullComputerName + Write-Message -Level VeryVerbose -Message "Getting SQL Server namespace on $Computer" -Target $Computer + try { $namespaces = Get-DbaCmObject -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -Query "Select Name FROM __NAMESPACE WHERE Name Like 'ComputerManagement%'" -EnableException -Credential $credential | Sort-Object Name -Descending } + catch { } + if ($namespaces) { + $servicesTemp = @() + + ForEach ($namespace in $namespaces) { + try { + Write-Message -Level Verbose -Message "Getting Cim class SqlService in Namespace $($namespace.Name) on $Computer." -Target $Computer + foreach ($service in (Get-DbaCmObject -ComputerName $Computer -Namespace "root\Microsoft\SQLServer\$($namespace.Name)" -Query "SELECT * FROM SqlService WHERE $searchClause" -EnableException -Credential $credential)) { + $servicesTemp += New-Object PSObject -Property @{ + Name = $service.ServiceName + Namespace = $namespace.Name + Service = $service + } + } + } + catch { + Write-Message -Level Verbose -EnableException $EnableException -Message "Failed to acquire services from namespace $($namespace.Name)." -Target $Computer + } + } - Gets the SQL Server related services of type "SSRS" (Reporting Services) on computers in the variable MyServers. + $services = ($servicesTemp | Group-Object Name | ForEach-Object { $_.Group | Sort-Object Namespace -Descending | Select-Object -First 1 }).Service -#> -[CmdletBinding()] -Param ( - [parameter(ValueFromPipeline = $true)] - [Alias("cn","host","Server")] - [string[]]$ComputerName = $env:COMPUTERNAME, - [PSCredential] [System.Management.Automation.CredentialAttribute()]$Credential, - [ValidateSet("Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS")][string]$Type -) + if ($services) { + Write-Message -Level Verbose -EnableException $EnableException -Message "Creating output objects" + ForEach ($service in $services) { + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name ComputerName -Value $service.HostName + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name ServiceType -Value ($ServiceIdMap | Where-Object { $_.Id -contains $service.SQLServiceType }).Name + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name State -Value $(switch ($service.State) { 1 { 'Stopped' } 2 { 'Start Pending' } 3 { 'Stop Pending' } 4 { 'Running' } }) + Add-Member -Force -InputObject $service -MemberType NoteProperty -Name StartMode -Value $(switch ($service.StartMode) { 1 { 'Unknown' } 2 { 'Automatic' } 3 { 'Manual' } 4 { 'Disabled' } }) -BEGIN - { - $FunctionName = (Get-PSCallstack)[0].Command - $ComputerName = $ComputerName | ForEach-Object {$_.split("\")[0]} | Select-Object -Unique - $TypeClause = switch($Type){ "Agent" {" = 2"} "Browser" {" = 7"} "Engine" {" = 1"} "FulText" {"= 3 OR SQLServiceType = 9"} "SSAS" {" = 5"} "SSIS" {" = 4"} "SSRS" {" = 6"} default {"> 0"} } - } -PROCESS - { - foreach ( $Computer in $ComputerName ) - { - $Server = Resolve-DbaNetworkName -ComputerName $Computer -Credential $credential - if ( $Server.ComputerName ) - { - $Computer = $server.ComputerName - Write-Verbose "$FunctionName - Getting SQL Server namespace on $Computer via CIM (WSMan)" - $namespace = Get-CimInstance -ComputerName $Computer -NameSpace root\Microsoft\SQLServer -ClassName "__NAMESPACE" -Filter "Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | - Where-Object {(Get-CimInstance -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -Query "SELECT * FROM SqlService" -ErrorAction SilentlyContinue).count -gt 0} | - Sort-Object Name -Descending | Select-Object -First 1 - if ( $namespace.Name ) - { - Write-Verbose "$FunctionName - Getting Cim class SqlService in Namespace $($namespace.Name) on $Computer via CIM (WSMan)" - try - { - Get-CimInstance -ComputerName $Computer -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -Query "SELECT * FROM SqlService WHERE SQLServiceType $TypeClause" -ErrorAction SilentlyContinue | - ForEach-Object { - [PSCustomObject]@{ - ComputerName = $_.HostName - ServiceName = $_.ServiceName - DisplayName = $_.DisplayName - StartName = $_.StartName - ServiceType = switch($_.SQLServiceType){1 {'Database Engine'} 2 {'SQL Agent'} 3 {'Full Text Search'} 4 {'SSIS'} 5 {'SSAS'} 6 {'SSRS'} 7 {'SQL Browser'} 8 {'Unknown'} 9 {'FullTextFilter Daemon Launcher'}} - State = switch($_.State){ 1 {'Stopped'} 2 {'Start Pending'} 3 {'Stop Pending' } 4 {'Running'}} - StartMode = switch($_.StartMode){ 1 {'Unknown'} 2 {'Automatic'} 3 {'Manual' } 4 {'Disabled'}} + if ($service.ServiceName -in ("MSSQLSERVER", "SQLSERVERAGENT", "ReportServer", "MSSQLServerOLAPService")) { + $instance = "MSSQLSERVER" + } + else { + if ($service.ServiceType -in @("Agent", "Engine", "SSRS", "SSAS")) { + if ($service.ServiceName.indexof('$') -ge 0) { + $instance = $service.ServiceName.split('$')[1] + } + else { + $instance = "Unknown" + } + } + else { + $instance = "" } } - } - catch - { - Write-Warning "$FunctionName - No Sql Services found on $Computer via CIM (WSMan)" - } - } - else - { - Write-Verbose "$FunctionName - Getting computer information from $Computer via CIMsession (DCOM)" - $sessionoption = New-CimSessionOption -Protocol DCOM - $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential - if ( $CIMSession ) - { - Write-Verbose "$FunctionName - Get ComputerManagement Namespace in CIMsession on $Computer with protocol DCom." - $namespace = Get-CimInstance -CimSession $CIMsession -NameSpace root\Microsoft\SQLServer -ClassName "__NAMESPACE" -Filter "Name Like 'ComputerManagement%'" -ErrorAction SilentlyContinue | - Where-Object {(Get-CimInstance -CimSession $CIMsession -Namespace $("root\Microsoft\SQLServer\" + $_.Name) -Query "SELECT * FROM SqlService" -ErrorAction SilentlyContinue).count -gt 0} | - Sort-Object Name -Descending | Select-Object -First 1 - } - else - { - Write-Warning "$FunctionName - can't create CIMsession via DCom on $Computer" - continue - } - if ( $namespace.Name ) - { - Write-Verbose "$FunctionName - Getting Cim class SqlService in Namespace $($namespace.Name) on $Computer via CIM (DCOM)" - try - { - Get-CimInstance -CimSession $CIMsession -Namespace $("root\Microsoft\SQLServer\" + $namespace.Name) -Query "SELECT * FROM SqlService WHERE SQLServiceType $TypeClause" -ErrorAction SilentlyContinue | - ForEach-Object { - [PSCustomObject]@{ - ComputerName = $_.HostName - ServiceName = $_.ServiceName - DisplayName = $_.DisplayName - StartName = $_.StartName - ServiceType = switch($_.SQLServiceType){1 {'Database Engine'} 2 {'SQL Agent'} 3 {'Full Text Search'} 4 {'SSIS'} 5 {'SSAS'} 6 {'SSRS'} 7 {'SQL Browser'} 8 {'Unknown'} 9 {'FullTextFilter Daemon Launcher'}} - State = switch($_.State){ 1 {'Stopped'} 2 {'Start Pending'} 3 {'Stop Pending' } 4 {'Running'}} - StartMode = switch($_.StartMode){ 1 {'Unknown'} 2 {'Automatic'} 3 {'Manual' } 4 {'Disabled'}} - } - } - } - catch - { - Write-Warning "$FunctionName - No Sql Services found on $Computer via CIM (DCOM)" + $priority = switch ($service.ServiceType) { + "Engine" { 200 } + default { 100 } + } + #If only specific instances are selected + if (!$InstanceName -or $instance -in $InstanceName) { + #Add other properties and methods + Add-Member -Force -InputObject $service -NotePropertyName InstanceName -NotePropertyValue $instance + Add-Member -Force -InputObject $service -NotePropertyName ServicePriority -NotePropertyValue $priority + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Stop" -Value { + Param ([bool]$Force = $false) + Stop-DbaSqlService -InputObject $this -Force:$Force + } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Start" -Value { Start-DbaSqlService -InputObject $this } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "Restart" -Value { + Param ([bool]$Force = $false) + Restart-DbaSqlService -InputObject $this -Force:$Force + } + Add-Member -Force -InputObject $service -MemberType ScriptMethod -Name "ChangeStartMode" -Value { + Param ( + [parameter(Mandatory = $true)] + [string]$Mode + ) + $supportedModes = @("Automatic", "Manual", "Disabled") + if ($Mode -notin $supportedModes) { + Stop-Function -Message ("Incorrect mode '$Mode'. Use one of the following values: {0}" -f ($supportedModes -join ' | ')) -EnableException $false -FunctionName 'Get-DbaSqlService' + Return + } + Set-ServiceStartMode -InputObject $this -Mode $Mode -ErrorAction Stop + $this.StartMode = $Mode + } + Select-DefaultView -InputObject $service -Property ComputerName, ServiceName, ServiceType, InstanceName, DisplayName, StartName, State, StartMode -TypeName DbaSqlService + } } - if ( $CIMsession ) { Remove-CimSession $CIMsession } - } - else - { - Write-Warning "$FunctionName - No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." - } + } + else { + Stop-Function -EnableException $EnableException -Message "No Sql Services found on $Computer" -Continue + } + } + else { + Stop-Function -EnableException $EnableException -Message "No ComputerManagement Namespace on $Computer. Please note that this function is available from SQL 2005 up." -Continue } } - else - { - Write-Warning "$FunctionName - Failed to connect to $Computer" + else { + Stop-Function -EnableException $EnableException -Message "Failed to connect to $Computer" -Continue } } } diff --git a/functions/Get-DbaSsisEnvironmentVariable.ps1 b/functions/Get-DbaSsisEnvironmentVariable.ps1 new file mode 100644 index 000000000000..ee81be2bb903 --- /dev/null +++ b/functions/Get-DbaSsisEnvironmentVariable.ps1 @@ -0,0 +1,257 @@ +function Get-DbaSsisEnvironmentVariable { + <# + .SYNOPSIS + This command gets specified SSIS Environment and all its variables + + .DESCRIPTION + This command gets all variables from specified environment from SSIS Catalog. All sensitive values are decrypted. + The function communicates directly with SSISDB database, "SQL Server Integration Services" service isn't queried there. + Each parameter (besides SqlInstance and SqlCredential) acts as the filter to only include or exclude particular element + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + This can be a collection and receive pipeline input to allow the function + to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Environment + The SSIS Environments names that we want to get variables from + + .PARAMETER EnvironmentExclude + The SSIS Environments to exclude. Acts as a filter for environments, best used without 'Environment' parameter + to get variables for all environments but excluded ones + + .PARAMETER Folder + The Folders names that contain the environments + + .PARAMETER FolderExclude + The Folders names to exclude. Acts as a filter for folders containing environments, best user without 'Folder' parameter + to get variables for all folders but excluded ones + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SSIS, SSISDB, Variable + Author: Bartosz Ratajczyk ( @b_ratajczyk ) + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaSsisEnvironmentVariable + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -Folder DWH_ETL + + Gets variables of 'DEV' environment located in 'DWH_ETL' folder on 'localhost' Server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -Folder DWH_ETL, DEV2, QA + + Gets variables of 'DEV' environment(s) located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV -FolderExclude DWH_ETL, DEV2, QA + + Gets variables of 'DEV' environments located in folders other than 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -Environment DEV, PROD -Folder DWH_ETL, DEV2, QA + + Gets variables of 'DEV' and 'PROD' environment(s) located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -EnvironmentExclude DEV, PROD -Folder DWH_ETL, DEV2, QA + + Gets variables of environments other than 'DEV' and 'PROD' located in folders 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + Get-DbaSsisEnvironmentVariable -SqlInstance localhost -EnvironmentExclude DEV, PROD -FolderExclude DWH_ETL, DEV2, QA + + Gets variables of environments other than 'DEV' and 'PROD' located in folders other than 'DWH_ETL', 'DEV2' and 'QA' on 'localhost' server + + .EXAMPLE + 'localhost' | Get-DbaSsisEnvironmentVariable -EnvironmentExclude DEV, PROD + + Gets all SSIS environments except 'DEV' and 'PROD' from 'localhost' server. The server name comes from pipeline + + .EXAMPLE + 'SRV1', 'SRV3' | Get-DbaSsisEnvironmentVariable + + Gets all SSIS environments from 'SRV1' and 'SRV3' servers. The server's names come from pipeline + + .EXAMPLE + 'SRV1', 'SRV2' | Get-DbaSsisEnvironmentVariable DEV | Out-GridView + + Gets all variables from 'DEV' Environment(s) on servers 'SRV1' and 'SRV2' and outputs it as the GridView. + The server names come from the pipeline. + + .EXAMPLE + 'localhost' | Get-DbaSsisEnvironmentVariable -EnvironmentExclude DEV, PROD | Select-Object -Property Name, Value | Where-Object {$_.Name -match '^a'} | Out-GridView + + Gets all variables from Environments other than 'DEV' and 'PROD' on 'localhost' server, + selects Name and Value properties for variables that names start with letter 'a' and outputs it as the GridView + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('SqlServer', 'ServerInstance')] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [parameter(Mandatory = $false)] + [object[]]$Environment, + [parameter(Mandatory = $false)] + [object[]]$EnvironmentExclude, + [parameter(Mandatory = $false)] + [object[]]$Folder, + [parameter(Mandatory = $false)] + [object[]]$FolderExclude, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Message "Connecting to $instance" -Level Verbose + $server = Connect-SqlInstance -SqlInstance $instance -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices" + + Write-Message -Message "Connecting to SSIS Catalog on $instance" -Level Verbose + $SSIS = New-Object "$ISNamespace.IntegrationServices" $server + } + catch { + Stop-Function -Message "Could not connect to SSIS Catalog on $instance or current SMO library does not support SSIS catalog" + return + } + + Write-Message -Message "Fetching SSIS Catalog and its folders" -Level Verbose + $catalog = $SSIS.Catalogs | Where-Object { $_.Name -eq "SSISDB" } + + # get all folders names if none provided + if ($null -eq $Folder) { + $searchFolders = $catalog.Folders.Name + } + else { + $searchFolders = $Folder + } + + # filter unwanted folders + if ($FolderExclude) { + $searchFolders = $searchFolders | Where-Object { $_ -notin $FolderExclude } + } + + if ($null -eq $searchFolders) { + Write-Message -Message "Instance: $instance > -Folder and -FolderExclude filters return an empty collection. Skipping" -Level Warning + } + else { + foreach ($f in $searchFolders) { + # get all environments names if none provided + if ($null -eq $Environment) { + $searchEnvironments = $catalog.Folders.Environments.Name + } + else { + $searchEnvironments = $Environment + } + + #filter unwanted environments + if ($EnvironmentExclude) { + $searchEnvironments = $searchEnvironments | Where-Object { $_ -notin $EnvironmentExclude } + } + + if ($null -eq $searchEnvironments) { + Write-Message -Message "Instance: $instance / Folder: $f > -Environment and -EnvironmentExclude filters return an empty collection. Skipping." -Level Warning + } + else { + $Environments = $catalog.Folders[$f].Environments | Where-Object { $_.Name -in $searchEnvironments } + + foreach ($e in $Environments) { + #encryption handling + $encKey = 'MS_Enckey_Env_' + $e.EnvironmentId + $encCert = 'MS_Cert_Env_' + $e.EnvironmentId + + <# + SMO does not return sensitive values (gets data from catalog.environment_variables) + We have to manually query internal.environment_variables instead and use symmetric keys + within T-SQL code + #> + + $sql = @" + OPEN SYMMETRIC KEY $encKey DECRYPTION BY CERTIFICATE $encCert; + + SELECT + ev.variable_id, + ev.name, + ev.description, + ev.type, + ev.sensitive, + value = ev.value, + ev.sensitive_value, + ev.base_data_type, + decrypted = decrypted.value + FROM internal.environment_variables ev + + CROSS APPLY ( + SELECT + value = CASE base_data_type + WHEN 'nvarchar' THEN CONVERT(NVARCHAR(MAX), DECRYPTBYKEY(sensitive_value)) + WHEN 'bit' THEN CONVERT(NVARCHAR(MAX), CONVERT(bit, DECRYPTBYKEY(sensitive_value))) + WHEN 'datetime' THEN CONVERT(NVARCHAR(MAX), CONVERT(datetime2(0), DECRYPTBYKEY(sensitive_value))) + WHEN 'single' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'float' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'decimal' THEN CONVERT(NVARCHAR(MAX), CONVERT(DECIMAL(38, 18), DECRYPTBYKEY(sensitive_value))) + WHEN 'tinyint' THEN CONVERT(NVARCHAR(MAX), CONVERT(tinyint, DECRYPTBYKEY(sensitive_value))) + WHEN 'smallint' THEN CONVERT(NVARCHAR(MAX), CONVERT(smallint, DECRYPTBYKEY(sensitive_value))) + WHEN 'int' THEN CONVERT(NVARCHAR(MAX), CONVERT(INT, DECRYPTBYKEY(sensitive_value))) + WHEN 'bigint' THEN CONVERT(NVARCHAR(MAX), CONVERT(bigint, DECRYPTBYKEY(sensitive_value))) + END + ) decrypted + WHERE environment_id = $($e.EnvironmentId); + CLOSE SYMMETRIC KEY $encKey; +"@ + + $ssisVariables = $server.Query($sql, "SSISDB") + + foreach ($variable in $ssisVariables) { + if ($variable.sensitive -eq $true) { + $value = $variable.decrypted + } + else { + $value = $variable.value + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Folder = $f + Environment = $e.Name + Id = $variable.variable_id + Name = $variable.Name + Description = $variable.description + Type = $variable.type + IsSensitive = $variable.sensitive + BaseDataType = $variable.base_data_type + Value = $value + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaStartupParameter.ps1 b/functions/Get-DbaStartupParameter.ps1 index d74b2feb9c22..aab285eefcfb 100644 --- a/functions/Get-DbaStartupParameter.ps1 +++ b/functions/Get-DbaStartupParameter.ps1 @@ -1,171 +1,191 @@ -Function Get-DbaStartupParameter -{ -<# -.SYNOPSIS -Displays values for a detailed list of SQL Server Startup Parameters. - -.DESCRIPTION -Displays values for a detailed list of SQL Server Startup Parameters including Master Data Path, Master Log path, Error Log, Trace Flags, Parameter String and much more. - -This command relies on remote Windows Server (SQL WMI/WinRm) access. You can pass alternative Windows credentials by using the -Credential parameter. - -See https://msdn.microsoft.com/en-us/library/ms190737.aspx for more information. - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the Windows Server as a different Windows user. - -.PARAMETER Simple -Shows a simplified output including only Server, Master Data Path, Master Log path, ErrorLog, TraceFlags and ParameterString - -.NOTES -Tags: WSMan, SQLWMI, Memory -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK - https://dbatools.io/Get-DbaStartupParameter - -.EXAMPLE -Get-DbaStartupParameter -SqlServer sql2014 - -Logs into SQL WMI as the current user then displays the values for numerous startup parameters. - -.EXAMPLE -$wincred = Get-Credential ad\sqladmin -Get-DbaStartupParameter -SqlServer sql2014 -Credential $wincred -Simple - -Logs in to WMI using the ad\sqladmin credential and gathers simplified information about the SQL Server Startup Parameters. - -#> - [CmdletBinding()] - param ([parameter(ValueFromPipeline, Mandatory = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [Alias("SqlCredential")] - [PSCredential]$Credential, - [switch]$Simple - ) - - PROCESS - { - foreach ($servername in $SqlServer) - { - $servercount++ - try - { - $instancename = ($servername.Split('\'))[1] - Write-Verbose "Attempting to connect to $servername" - - if ($instancename.Length -eq 0) { $instancename = "MSSQLSERVER" } - - $displayname = "SQL Server ($instancename)" - - $Scriptblock = { - $servername = $args[0] - $displayname = $args[1] - - $wmisvc = $wmi.Services | Where-Object { $_.DisplayName -eq $displayname } - - $params = $wmisvc.StartupParameters -split ';' - - $masterdata = $params | Where-Object { $_.StartsWith('-d') } - $masterlog = $params | Where-Object { $_.StartsWith('-l') } - $errorlog = $params | Where-Object { $_.StartsWith('-e') } - $traceflags = $params | Where-Object { $_.StartsWith('-T') } - - $debugflag = $params | Where-Object { $_.StartsWith('-t') } - - if ($debugflag.length -ne 0) - { - Write-Warning "$servername is using the lowercase -t trace flag. This is for internal debugging only. Please ensure this was intentional." - } - - if ($traceflags.length -eq 0) - { - $traceflags = "None" - } - - if ($Simple -eq $true) - { - [PSCustomObject]@{ - Server = $Servername - MasterData = $masterdata.TrimStart('-d') - MasterLog = $masterlog.TrimStart('-l') - ErrorLog = $errorlog.TrimStart('-e') - TraceFlags = $traceflags -join ',' - ParameterString = $wmisvc.StartupParameters - } - } - else - { - # From https://msdn.microsoft.com/en-us/library/ms190737.aspx - - $commandpromptparm = $params | Where-Object { $_ -eq '-c' } - $minimalstartparm = $params | Where-Object { $_ -eq '-f' } - $memorytoreserve = $params | Where-Object { $_.StartsWith('-g') } - $noeventlogsparm = $params | Where-Object { $_ -eq '-n' } - $instancestartparm = $params | Where-Object { $_ -eq '-s' } - $disablemonitoringparm = $params | Where-Object { $_ -eq '-x' } - $increasedextentsparm = $params | Where-Object { $_ -ceq '-E' } - - $minimalstart = $noeventlogs = $instancestart = $disablemonitoring = $false - $increasedextents = $commandprompt = $singleuser = $false - - if ($commandpromptparm -ne $null) { $commandprompt = $true } - if ($minimalstartparm -ne $null) { $minimalstart = $true } - if ($memorytoreserve -eq $null) { $memorytoreserve = 0 } - if ($noeventlogsparm -ne $null) { $noeventlogs = $true } - if ($instancestartparm -ne $null) { $instancestart = $true } - if ($disablemonitoringparm -ne $null) { $disablemonitoring = $true } - if ($increasedextentsparm -ne $null) { $increasedextents = $true } - - $singleuserparm = $params | Where-Object { $_.StartsWith('-m') } - - if ($singleuserparm.length -ne 0) - { - $singleuser = $true - $singleuserdetails = $singleuserparm.TrimStart('-m') - # It's possible the person specified an application name - # if not, just say that single user is $true - if ($singleuserdetails.length -eq 0) - { - $singleuser = $singleuserdetails - } - } - - [PSCustomObject]@{ - Server = $Servername - MasterData = $masterdata.TrimStart('-d') - MasterLog = $masterlog.TrimStart('-l') - ErrorLog = $errorlog.TrimStart('-e') - TraceFlags = $traceflags -join ',' - CommandPromptStart = $commandprompt - MinimalStart = $minimalstart - MemoryToReserve = $memorytoreserve - SingleUser = $singleuser - NoLoggingToWinEvents = $noeventlogs - StartAsNamedInstance = $instancestart - DisableMonitoring = $disablemonitoring - IncreasedExtents = $increasedextents - ParameterString = $wmisvc.StartupParameters - } - } - } - - # This command is in the internal function - # It's sorta like Invoke-Command. - Invoke-ManagedComputerCommand -ComputerName $servername -Credential $credential -ScriptBlock $Scriptblock -ArgumentList $servername, $displayname - } - catch - { - Write-Warning "$servername`: $_ " - } - } - } +function Get-DbaStartupParameter { + <# + .SYNOPSIS + Displays values for a detailed list of SQL Server Startup Parameters. + + .DESCRIPTION + Displays values for a detailed list of SQL Server Startup Parameters including Master Data Path, Master Log path, Error Log, Trace Flags, Parameter String and much more. + + This command relies on remote Windows Server (SQL WMI/WinRm) access. You can pass alternative Windows credentials by using the -Credential parameter. + + See https://msdn.microsoft.com/en-us/library/ms190737.aspx for more information. + + .PARAMETER SqlInstance + The SQL Server instance to connect to. + + .PARAMETER Credential + Allows you to login to servers using alternate Windows credentials. + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Simple + If this switch is enabled, simplified output will be produced including only Server, Master Data Path, Master Log path, ErrorLog, TraceFlags and ParameterString. + + .PARAMETER EnableException + If this switch is enabled, exceptions will be thrown to the caller, which will need to perform its own exception processing. Otherwise, the function will try to catch the exception, interpret it and provide a friendly error message. + + .EXAMPLE + Get-DbaStartupParameter -SqlInstance sql2014 + + Logs into SQL WMI as the current user then displays the values for numerous startup parameters. + + .EXAMPLE + $wincred = Get-Credential ad\sqladmin + Get-DbaStartupParameter -SqlInstance sql2014 -Credential $wincred -Simple + + Logs in to WMI using the ad\sqladmin credential and gathers simplified information about the SQL Server Startup Parameters. + + .NOTES + Tags: WSMan, SQLWMI, Memory + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaStartupParameter +#> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("SqlCredential")] + [PSCredential]$Credential, + [switch]$Simple, + [switch] + [Alias('Silent')]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $computerName = $instance.ComputerName + $instanceName = $instance.InstanceName + $ogInstance = $instance.FullSmoName + + $computerName = (Resolve-DbaNetworkName -ComputerName $computerName).FullComputerName + + Write-Message -Level Verbose -message "Attempting to connect to $computerName" + + if ($instanceName.Length -eq 0) { $instanceName = "MSSQLSERVER" } + + $displayname = "SQL Server ($instanceName)" + + $Scriptblock = { + $computerName = $args[0] + $displayname = $args[1] + + $wmisvc = $wmi.Services | Where-Object DisplayName -eq $displayname + + $params = $wmisvc.StartupParameters -split ';' + + $masterdata = $params | Where-Object { $_.StartsWith('-d') } + $masterlog = $params | Where-Object { $_.StartsWith('-l') } + $errorlog = $params | Where-Object { $_.StartsWith('-e') } + $traceflags = $params | Where-Object { $_.StartsWith('-T') } + + $debugflag = $params | Where-Object { $_.StartsWith('-t') } + + if ($debugflag.length -ne 0) { + Write-Message -Level Warning "$instance is using the lowercase -t trace flag. This is for internal debugging only. Please ensure this was intentional." + } + #> + + if ($traceflags.length -eq 0) { + $traceflags = "None" + } + else { + $traceflags = $traceflags.substring(2) + } + + if ($Simple -eq $true) { + [PSCustomObject]@{ + ComputerName = $computerName + InstanceName = $instanceName + SqlInstance = $ogInstance + MasterData = $masterdata.TrimStart('-d') + MasterLog = $masterlog.TrimStart('-l') + ErrorLog = $errorlog.TrimStart('-e') + TraceFlags = $traceflags -join ',' + ParameterString = $wmisvc.StartupParameters + } + } + else { + # From https://msdn.microsoft.com/en-us/library/ms190737.aspx + + $commandpromptparm = $params | Where-Object { $_ -eq '-c' } + $minimalstartparm = $params | Where-Object { $_ -eq '-f' } + $memorytoreserve = $params | Where-Object { $_.StartsWith('-g') } + $noeventlogsparm = $params | Where-Object { $_ -eq '-n' } + $instancestartparm = $params | Where-Object { $_ -eq '-s' } + $disablemonitoringparm = $params | Where-Object { $_ -eq '-x' } + $increasedextentsparm = $params | Where-Object { $_ -ceq '-E' } + + $minimalstart = $noeventlogs = $instancestart = $disablemonitoring = $false + $increasedextents = $commandprompt = $singleuser = $false + + if ($null -ne $commandpromptparm) { + $commandprompt = $true + } + if ($null -ne $minimalstartparm) { + $minimalstart = $true + } + if ($null -eq $memorytoreserve) { + $memorytoreserve = 0 + } + if ($null -ne $noeventlogsparm) { + $noeventlogs = $true + } + if ($null -ne $instancestartparm) { + $instancestart = $true + } + if ($null -ne $disablemonitoringparm) { + $disablemonitoring = $true + } + if ($null -ne $increasedextentsparm) { + $increasedextents = $true + } + + $singleuserparm = $params | Where-Object { $_.StartsWith('-m') } + + if ($singleuserparm.length -ne 0) { + $singleuser = $true + $singleuserdetails = $singleuserparm.TrimStart('-m') + } + + [PSCustomObject]@{ + ComputerName = $computerName + InstanceName = $instanceName + SqlInstance = $ogInstance + MasterData = $masterdata -replace '^-[dD]', '' + MasterLog = $masterlog -replace '^-[lL]', '' + ErrorLog = $errorlog -replace '^-[eE]', '' + TraceFlags = $traceflags -join ',' + CommandPromptStart = $commandprompt + MinimalStart = $minimalstart + MemoryToReserve = $memorytoreserve + SingleUser = $singleuser + SingleUserName = $singleuserdetails + NoLoggingToWinEvents = $noeventlogs + StartAsNamedInstance = $instancestart + DisableMonitoring = $disablemonitoring + IncreasedExtents = $increasedextents + ParameterString = $wmisvc.StartupParameters + } + } + } + + # This command is in the internal function + # It's sorta like Invoke-Command. + if ($credential) { + Invoke-ManagedComputerCommand -Server $computerName -Credential $credential -ScriptBlock $Scriptblock -ArgumentList $computerName, $displayname + } + else { + Invoke-ManagedComputerCommand -Server $computerName -ScriptBlock $Scriptblock -ArgumentList $computerName, $displayname + } + } + catch { + Stop-Function -Message "$instance failed." -ErrorRecord $_ -Continue -Target $instance + } + } + } } diff --git a/functions/Get-DbaSuspectPage.ps1 b/functions/Get-DbaSuspectPage.ps1 new file mode 100644 index 000000000000..6f3dc189ef4a --- /dev/null +++ b/functions/Get-DbaSuspectPage.ps1 @@ -0,0 +1,106 @@ +function Get-DbaSuspectPage { + <# + .SYNOPSIS + Returns data that is stored in SQL for Suspect Pages on the specified SQL Server Instance + + .DESCRIPTION + This function returns any records that were stored due to suspect pages in databases on a SQL Server Instance. + + .PARAMETER SqlInstance + A SQL Server instance to connect to + + .PARAMETER SqlCredential + A credential to use to connect to the SQL Instance rather than using Windows Authentication + + .PARAMETER Database + The database to return. If unspecified, all records will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Pages, DBCC + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaSuspectPage -SqlInstance sql2016 + + Retrieve any records stored for Suspect Pages on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaSuspectPage -SqlInstance sql2016 -Database Test + + Retrieve any records stored for Suspect Pages on the sql2016 SQL Server and the Test database only. + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [object]$Database, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlinstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + $sql = "Select + DB_NAME(database_id) as DBName, + file_id, + page_id, + CASE event_type + WHEN 1 THEN '823 or 824 or Torn Page' + WHEN 2 THEN 'Bad Checksum' + WHEN 3 THEN 'Torn Page' + WHEN 4 THEN 'Restored' + WHEN 5 THEN 'Repaired (DBCC)' + WHEN 7 THEN 'Deallocated (DBCC)' + END as EventType, + error_count, + last_update_date + from msdb.dbo.suspect_pages" + + try { + $results = $server.Query($sql) + } + catch { + Stop-Function -Message "Issue collecting data on $server" -Target $server -ErrorRecord $_ -Continue + } + + if ($Database) { + $results = $results | Where-Object DBName -EQ $Database + } + + } + foreach ($row in $results) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $row.DBName + FileId = $row.file_id + PageId = $row.page_id + EventType = $row.EventType + ErrorCount = $row.error_count + LastUpdateDate = $row.last_update_date + } + } + } +} diff --git a/functions/Get-DbaTable.ps1 b/functions/Get-DbaTable.ps1 new file mode 100644 index 000000000000..ad54132af4f2 --- /dev/null +++ b/functions/Get-DbaTable.ps1 @@ -0,0 +1,202 @@ +function Get-DbaTable { + <# +.SYNOPSIS +Returns a summary of information on the tables + +.DESCRIPTION +Shows table information around table row and data sizes and if it has any table type information. + +.PARAMETER SqlInstance +SQL Server name or SMO object representing the SQL Server to connect to. This can be a +collection and receive pipeline input + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server + +.PARAMETER IncludeSystemDBs +Switch parameter that when used will display system database information + +.PARAMETER Table +Define a specific table you would like to query. You can specify up to three-part name like db.sch.tbl. +If the object has special characters please wrap them in square brackets [ ]. +This dbo.First.Table will try to find table named 'Table' on schema 'First' and database 'dbo'. +The correct way to find table named 'First.Table' on schema 'dbo' is passing dbo.[First.Table] + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database, Tables +Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Get-DbaTable + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Database Test1 +Return all tables in the Test1 database + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Database MyDB -Table MyTable +Return only information on the table MyTable from the database MyDB + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Table MyTable +Returns information on table called MyTable if it exists in any database on the server, under any schema + +.EXAMPLE +Get-DbaTable -SqlInstance DEV01 -Table dbo.[First.Table] +Returns information on table called First.Table on schema dbo if it exists in any database on the server + +.EXAMPLE +'localhost','localhost\namedinstance' | Get-DbaTable -Database DBA -Table Commandlog +Returns information on the CommandLog table in the DBA database on both instances localhost and the named instance localhost\namedinstance + +#> + [CmdletBinding()] + param ([parameter(ValueFromPipeline, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystemDBs, + [string[]]$Table, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($Table) { + $fqtns = @() + foreach ($t in $Table) { + $splitName = [regex]::Matches($t, "(\[.+?\])|([^\.]+)").Value + $dotcount = $splitName.Count + + $splitDb = $Schema = $null + + switch ($dotcount) { + 1 { + $tbl = $t + } + 2 { + $schema = $splitName[0] + $tbl = $splitName[1] + } + 3 { + $splitDb = $splitName[0] + $schema = $splitName[1] + $tbl = $splitName[2] + } + default { + Write-Message -Level Warning -Message "Please make sure that you are using up to three-part names. If your search value contains '.' character you must use [ ] to wrap the name. The value $t is not a valid name." + Continue + } + } + + if ($splitDb -like "[[]*[]]") { + $splitDb = $splitDb.Substring(1, ($splitDb.Length - 2)) + } + + if ($schema -like "[[]*[]]") { + $schema = $schema.Substring(1, ($schema.Length - 2)) + } + + if ($tbl -like "[[]*[]]") { + $tbl = $tbl.Substring(1, ($tbl.Length - 2)) + } + + $fqtns += [PSCustomObject] @{ + Database = $splitDb + Schema = $Schema + Table = $tbl + } + } + } + } + + process { + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + #only look at online databases (Status equal normal) + $dbs = $server.Databases | Where-Object IsAccessible + + #If IncludeSystemDBs is false, exclude systemdbs + if (!$IncludeSystemDBs -and !$Database) { + $dbs = $dbs | Where-Object { !$_.IsSystemObject } + } + + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + catch { + Stop-Function -Message "Unable to gather dbs for $instance" -Target $instance -Continue -ErrorRecord $_ + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db" + + if ($fqtns) { + $tables = @() + foreach ($fqtn in $fqtns) { + # If the user specified a database in a three-part name, and it's not the + # database currently being processed, skip this table. + if ($fqtn.Database) { + if ($fqtn.Database -ne $db.Name) { + continue + } + } + + $tbl = $db.tables | Where-Object { $_.Name -in $fqtn.Table -and $fqtn.Schema -in ($_.Schema, $null) -and $fqtn.Database -in ($_.Parent.Name, $null) } + + if (-not $tbl) { + Write-Message -Level Verbose -Message "Could not find table $($fqtn.Table) in $db on $server" + } + $tables += $tbl + } + } + else { + $tables = $db.Tables + } + + foreach ($sqltable in $tables) { + $sqltable | Add-Member -Force -MemberType NoteProperty -Name ComputerName -Value $server.NetName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + $sqltable | Add-Member -Force -MemberType NoteProperty -Name Database -Value $db.Name + + $defaultprops = "ComputerName", "InstanceName", "SqlInstance", "Database", "Schema", "Name", "IndexSpaceUsed", "DataSpaceUsed", "RowCount", "HasClusteredIndex", "IsFileTable", "IsMemoryOptimized", "IsPartitioned", "FullTextIndex", "ChangeTrackingEnabled" + + Select-DefaultView -InputObject $sqltable -Property $defaultprops + } + } + } + } +} diff --git a/functions/Get-DbaTcpPort.ps1 b/functions/Get-DbaTcpPort.ps1 index d88272c66380..4abedd2e0506 100644 --- a/functions/Get-DbaTcpPort.ps1 +++ b/functions/Get-DbaTcpPort.ps1 @@ -1,198 +1,199 @@ -Function Get-DbaTcpPort -{ -<# -.SYNOPSIS -Returns the TCP port used by the specified SQL Server. - -.DESCRIPTION -By default, this command returns just the TCP port used by the specified SQL Server. - -If -Detailed is specified, server name, IPAddress (ipv4 and ipv6), port number and if the port assignment is static. - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Detailed -Returns an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for one or more SQL Servers. - -Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. - -.PARAMETER NoIpv6 -Excludes IPv6 information when -Detailed is specified. - -.NOTES -Tags: SQLWMI -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Get-DbaTcpPort - -.EXAMPLE -Get-DbaTcpPort -SqlServer sqlserver2014a - -Returns just the port number for the default instance on sqlserver2014a - -.EXAMPLE -Get-DbaTcpPort -SqlServer winserver\sqlexpress, sql2016 - -Returns an object with server name and port number for the sqlexpress on winserver and the default instance on sql2016 - -.EXAMPLE -Get-DbaTcpPort -SqlServer sqlserver2014a, sql2016 -Detailed - -Returns an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for sqlserver2014a and sql2016 - -Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sql2014 | Get-DbaTcpPort -NoIpV6 -Detailed -Verbose - -Returns an object with server name, IPAddress (just ipv4), port and static ($true/$false) for every server listed in the Central Management Server on sql2014 - -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [string[]]$SqlServer, - [Alias("SqlCredential")] - [PsCredential]$Credential, - [switch]$Detailed, - [Alias("Ipv4")] - [switch]$NoIpv6 - ) - - PROCESS - { - foreach ($servername in $SqlServer) - { - if ($detailed -eq $true) - { - try - { - $scriptblock = { - $servername = $args[0] - - Add-Type -AssemblyName Microsoft.VisualBasic - $wmi = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer $servername - - foreach ($instance in $wmi.ServerInstances) - { - $allips = @() - $instancename = $instance.name - - $tcp = $instance.ServerProtocols | Where-Object { $_.DisplayName -eq "TCP/IP" } - $ips = $tcp.IPAddresses - - Write-Verbose "Parsing information for $($ips.count) IP addresses" - foreach ($ip in $ips) - { - $props = $ip.IPAddressProperties | Where-Object { $_.Name -eq "TcpPort" -or $_.Name -eq "TcpDynamicPorts" } - - foreach ($prop in $props) - { - if ([Microsoft.VisualBasic.Information]::IsNumeric($prop.value)) - { - $port = $prop.value - if ($prop.name -eq 'TcpPort') - { - $static = $true - } - else - { - $static = $false - } - break - } - } - - [PsCustomObject]@{ - ComputerName = $servername - InstanceName = $instancename - IPAddress = $ip.Ipaddress.IPAddressToString - Port = $port - Static = $static - } - } - } - } - - $resolved = Resolve-DbaNetworkName -ComputerName $servername -Verbose:$false - $fqdn = $resolved.FQDN - $computername = $resolved.ComputerName - try - { - Write-Verbose "Trying with ComputerName ($computername)" - $someips = Invoke-ManagedComputerCommand -ComputerName $computername -ArgumentList $computername -ScriptBlock $scriptblock - } - catch - { - Write-Verbose "Trying with FQDN because ComputerName failed" - $someips = Invoke-ManagedComputerCommand -ComputerName $fqdn -ArgumentList $fqdn -ScriptBlock $scriptblock - } - } - catch - { - Write-Warning "Could not get detailed information for $servername" - Write-Warning $_.Exception.Message - } - - $cleanedup = $someips | Sort-Object IPAddress - - if ($NoIpv6) - { - $octet = '(?:0?0?[0-9]|0?[1-9][0-9]|1[0-9]{2}|2[0-5][0-5]|2[0-4][0-9])' - [regex]$ipv4 = "^(?:$octet\.){3}$octet$" - $cleanedup = $cleanedup | Where-Object { $_.IPAddress -match $ipv4 } - } - - $cleanedup - } - - if ($Detailed -eq $false -or ($Detailed -eq $true -and $someips -eq $null)) - { - try - { - $server = Connect-SqlServer -SqlServer "TCP:$servername" -SqlCredential $Credential - } - catch - { - Write-Warning "Can't connect to $servername. Moving on." - Continue - } - - if ($server.VersionMajor -lt 9) - { - if ($servercount -eq 1) - { - throw "SQL Server 2000 not supported." - } - else - { - Write-Warning "SQL Server 2000 not supported. Skipping $servername." - Continue - } - } - - # WmiComputer can be unreliable :( Use T-SQL - $sql = "SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID" - $port = $server.ConnectionContext.ExecuteScalar($sql) - - [PSCustomObject]@{ - Server = $servername - Port = $port - } - } - } - } +function Get-DbaTcpPort { + <# + .SYNOPSIS + Returns the TCP port used by the specified SQL Server. + + .DESCRIPTION + By default, this function returns just the TCP port used by the specified SQL Server. + + If -Detailed is specified, the server name, IPAddress (ipv4 and ipv6), port number and an indicator of whether or not the port assignment is static are returned. + + Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Allows you to connect to servers using alternate Windows credentials + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + .PARAMETER Detailed + If this switch is enabled, an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for one or more SQL Servers is returned. + + .PARAMETER ExcludeIpv6 + If this switch is enabled, IPv6 information is excluded from detailed output. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SQLWMI, tcp + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTcpPort + + .EXAMPLE + Get-DbaTcpPort -SqlInstance sqlserver2014a + + Returns just the port number for the default instance on sqlserver2014a. + + .EXAMPLE + Get-DbaTcpPort -SqlInstance winserver\sqlexpress, sql2016 + + Returns an object with server name and port number for the sqlexpress on winserver and the default instance on sql2016. + + .EXAMPLE + Get-DbaTcpPort -SqlInstance sqlserver2014a, sql2016 -Detailed + + Returns an object with server name, IPAddress (ipv4 and ipv6), port and static ($true/$false) for sqlserver2014a and sql2016. + + Remote sqlwmi is used by default. If this doesn't work, then remoting is used. If neither work, it defaults to T-SQL which can provide only the port. + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaTcpPort -ExcludeIpv6 -Detailed + + Returns an object with server name, IPAddress (just ipv4), port and static ($true/$false) for every server listed in the Central Management Server on sql2014. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [switch]$Detailed, + [Alias("Ipv4")] + [switch]$ExcludeIpv6, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + if ($detailed -eq $true) { + try { + $scriptblock = { + $instance = $args[0] + + Add-Type -AssemblyName Microsoft.VisualBasic + + foreach ($servername in $wmi.ServerInstances) { + $instanceName = $servername.Name + $wmiinstance = $wmi.Services | Where-Object { $_.DisplayName -eq "SQL Server ($instanceName)" } + $vsname = ($wmiinstance.AdvancedProperties | Where-Object { $_ -match 'VSNAME' }).Value + + if ($vsname.length -eq 0) { + $vsname = "$instance\$instanceName" + } + + $vsname = $vsname.Replace("\MSSQLSERVER", "") + + try { + $regroot = ($wmiinstance.AdvancedProperties | Where-Object { $_ -match 'REGROOT' }).Value + $dacport = (Get-ItemProperty "HKLM:\$regroot\MSSQLServer\SuperSocketNetLib\AdminConnection\Tcp").TcpDynamicPorts + + [PsCustomObject]@{ + ComputerName = $instance + InstanceName = $instanceName + SqlInstance = $vsname + IPAddress = "0.0.0.0" + Port = $dacport + Static = $false + Type = "DAC" + } + } + catch { + # it's just not our day + } + + $tcp = $servername.ServerProtocols | Where-Object Name -eq Tcp + $ips = $tcp.IPAddresses + + # This is a remote command so do not use Write-message + Write-Verbose "Parsing information for $($ips.count) IP addresses." + foreach ($ip in $ips) { + $props = $ip.IPAddressProperties | Where-Object { $_.Name -eq "TcpPort" -or $_.Name -eq "TcpDynamicPorts" } + + foreach ($prop in $props) { + if ([Microsoft.VisualBasic.Information]::IsNumeric($prop.value)) { + $port = $prop.value + if ($prop.name -eq 'TcpPort') { + $static = $true + } + else { + $static = $false + } + break + } + } + + [PsCustomObject]@{ + ComputerName = $instance + InstanceName = $instanceName + SqlInstance = $vsname + IPAddress = $ip.IPAddress.IPAddressToString + Port = $port + Static = $static + Type = "Normal" + } + } + } + } + + $computer = $instance.ComputerName + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Verbose:$false + $computername = $resolved.FullComputerName + + try { + Write-Message -Level Verbose -Message "Trying with ComputerName ($computer)." + $someIps = Invoke-ManagedComputerCommand -ComputerName $computer -ArgumentList $computer -ScriptBlock $scriptblock + } + catch { + Write-Message -Level Verbose -Message "Trying with FullComputerName because ComputerName failed." + $someIps = Invoke-ManagedComputerCommand -ComputerName $computername -ArgumentList $fqdn -ScriptBlock $scriptblock + } + } + catch { + Stop-Function -Message "Could not get detailed information." -Target $instance -ErrorRecord $_ + } + + $cleanedUp = $someIps | Sort-Object IPAddress + + if ($ExcludeIpv6) { + $octet = '(?:0?0?[0-9]|0?[1-9][0-9]|1[0-9]{2}|2[0-5][0-5]|2[0-4][0-9])' + [regex]$ipv4 = "^(?:$octet\.){3}$octet$" + $cleanedUp = $cleanedUp | Where-Object { $_.IPAddress -match $ipv4 } + } + + $cleanedUp + } + + if ($Detailed -eq $false -or ($Detailed -eq $true -and $null -eq $someIps)) { + try { + $server = Connect-SqlInstance -SqlInstance "TCP:$instance" -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $servername -Continue + } + + # WmiComputer can be unreliable :( Use T-SQL + $sql = "SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID" + $port = $server.Query($sql) + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Port = $port.local_tcp_port + } + } + } + } } diff --git a/functions/Get-DbaTempdbUsage.ps1 b/functions/Get-DbaTempdbUsage.ps1 new file mode 100644 index 000000000000..6505f34d1e13 --- /dev/null +++ b/functions/Get-DbaTempdbUsage.ps1 @@ -0,0 +1,132 @@ +function Get-DbaTempdbUsage { + <# + .SYNOPSIS + Gets Tempdb usage for running queries. + + .DESCRIPTION + This function queries DMVs for running sessions using Tempdb and returns results if those sessions have user or internal space allocated or deallocated against them. + + .PARAMETER SqlInstance + The SQL Instance you are querying against. + + .PARAMETER SqlCredential + If you want to use alternative credentials to connect to the server. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Tempdb, Space + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTempdbUsage + + .EXAMPLE + Get-DbaTempdbUsage -SqlInstance localhost\SQLDEV2K14 + + Gets tempdb usage for localhost\SQLDEV2K14 + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -le 9) { + Stop-Function -Message "This function is only supported in SQL Server 2008 or higher." -Continue + } + + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + t.session_id AS Spid, + r.command AS StatementCommand, + SUBSTRING( est.[text], + (r.statement_start_offset / 2) + 1, + ((CASE r.statement_end_offset + WHEN-1 + THEN DATALENGTH(est.[text]) + ELSE + r.statement_end_offset + END - r.statement_start_offset + ) / 2 + ) + 1 + ) AS QueryText, + QUOTENAME(DB_NAME(r.database_id)) + N'.' + QUOTENAME(OBJECT_SCHEMA_NAME(est.objectid, est.dbid)) + N'.' + + QUOTENAME(OBJECT_NAME(est.objectid, est.dbid)) AS ProcedureName, + r.start_time AS StartTime, + tdb.UserObjectAllocated * 8 AS CurrentUserAllocatedKB, + (t.user_objects_alloc_page_count + tdb.UserObjectAllocated) * 8 AS TotalUserAllocatedKB, + tdb.UserObjectDeallocated * 8 AS UserDeallocatedKB, + (t.user_objects_dealloc_page_count + tdb.UserObjectDeallocated) * 8 AS TotalUserDeallocatedKB, + tdb.InternalObjectAllocated * 8 AS InternalAllocatedKB, + (t.internal_objects_alloc_page_count + tdb.InternalObjectAllocated) * 8 AS TotalInternalAllocatedKB, + tdb.InternalObjectDeallocated * 8 AS InternalDeallocatedKB, + (t.internal_objects_dealloc_page_count + tdb.InternalObjectDeallocated) * 8 AS TotalInternalDeallocatedKB, + r.reads AS RequestedReads, + r.writes AS RequestedWrites, + r.logical_reads AS RequestedLogicalReads, + r.cpu_time AS RequestedCPUTime, + s.is_user_process AS IsUserProcess, + s.[status] AS [Status], + DB_NAME(r.database_id) AS [Database], + s.login_name AS LoginName, + s.original_login_name AS OriginalLoginName, + s.nt_domain AS NTDomain, + s.nt_user_name AS NTUserName, + s.[host_name] AS HostName, + s.[program_name] AS ProgramName, + s.login_time AS LoginTime, + s.last_request_start_time AS LastRequestedStartTime, + s.last_request_end_time AS LastRequestedEndTime +FROM sys.dm_db_session_space_usage AS t +INNER JOIN sys.dm_exec_sessions AS s + ON s.session_id = t.session_id +LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id +LEFT JOIN + ( SELECT _tsu.session_id, + _tsu.request_id, + SUM(_tsu.user_objects_alloc_page_count) AS UserObjectAllocated, + SUM(_tsu.user_objects_dealloc_page_count) AS UserObjectDeallocated, + SUM(_tsu.internal_objects_alloc_page_count) AS InternalObjectAllocated, + SUM(_tsu.internal_objects_dealloc_page_count) AS InternalObjectDeallocated + FROM tempdb.sys.dm_db_task_space_usage AS _tsu + GROUP BY _tsu.session_id, + _tsu.request_id + ) AS tdb + ON tdb.session_id = r.session_id + AND tdb.request_id = r.request_id +OUTER APPLY sys.dm_exec_sql_text(r.[sql_handle]) AS est +WHERE t.session_id != @@SPID + AND (tdb.UserObjectAllocated - tdb.UserObjectDeallocated + tdb.InternalObjectAllocated - tdb.InternalObjectDeallocated) != 0 +OPTION (RECOMPILE);" + + $server.Query($sql) + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaTopResourceUsage.ps1 b/functions/Get-DbaTopResourceUsage.ps1 new file mode 100644 index 000000000000..93204e1c9765 --- /dev/null +++ b/functions/Get-DbaTopResourceUsage.ps1 @@ -0,0 +1,301 @@ +function Get-DbaTopResourceUsage { + <# + .SYNOPSIS + Returns the top 20 resource consumers for cached queries based on four different metrics: duration, frequency, IO, and CPU. + + .DESCRIPTION + Returns the top 20 resource consumers for cached queries based on four different metrics: duration, frequency, IO, and CPU. + + This command is based off of queries provided by Michael J. Swart at http://michaeljswart.com/go/Top20 + + Per Michael: "I've posted queries like this before, and others have written many other versions of this query. All these queries are based on sys.dm_exec_query_stats." + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER ExcludeSystem + This will exclude system objects like replication procedures from being returned. + + .PARAMETER Type + By default, all Types run but you can specify one or more of the following: Duration, Frequency, IO, or CPU + + .PARAMETER Limit + By default, these query the Top 20 worst offenders (though more than 20 results can be returend if each of the top 20 have more than 1 subsequent result) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Query, Performance + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTopResourceUsage + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008, sql2012 + Return the 80 (20 x 4 types) top usage results by duration, frequency, IO, and CPU servers for servers sql2008 and sql2012 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008 -Type Duration, Frequency -Database TestDB + Return the highest usage by duration (top 20) and frequency (top 20) for the TestDB on sql2008 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2016 -Limit 30 + Return the highest usage by duration (top 30) and frequency (top 30) for the TestDB on sql2016 + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2008, sql2012 -ExcludeSystem + Return the 80 (20 x 4 types) top usage results by duration, frequency, IO, and CPU servers for servers sql2008 and sql2012 without any System Objects + + + .EXAMPLE + Get-DbaTopResourceUsage -SqlInstance sql2016| Select * + Return all the columns plus the QueryPlan column + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [ValidateSet("All", "Duration", "Frequency", "IO", "CPU")] + [string[]]$Type = "All", + [int]$Limit = 20, + [Alias('Silent')] + [switch]$EnableException, + [switch]$ExcludeSystem + ) + + begin { + + $instancecolumns = " SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, " + + if ($database) { + $wheredb = " and coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') in ('$($database -join '', '')')" + } + + if ($ExcludeDatabase) { + $wherenotdb = " and coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') notin '$($excludedatabase -join '', '')'" + } + + if ($ExcludeSystem) { + $whereexcludesystem = " AND coalesce(object_name(st.objectid, st.dbid), '') NOT LIKE 'sp_MS%' " + } + $duration = ";with long_queries as + ( + select top $Limit + query_hash, + sum(total_elapsed_time) elapsed_time + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_elapsed_time) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_elapsed_time / 1000 as TotalElapsedTimeMs, + qs.execution_count as ExecutionCount, + cast((total_elapsed_time / 1000) / (execution_count + 0.0) as money) as AverageDurationMs, + lq.elapsed_time / 1000 as QueryTotalElapsedTimeMs, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join long_queries lq + on lq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by lq.elapsed_time desc, + lq.query_hash, + qs.total_elapsed_time desc + option (recompile)" + + $frequency = ";with frequent_queries as + ( + select top $Limit + query_hash, + sum(execution_count) executions + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(execution_count) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.execution_count as ExecutionCount, + executions as QueryTotalExecutions, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join frequent_queries fq + on fq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by fq.executions desc, + fq.query_hash, + qs.execution_count desc + option (recompile)" + + $io = ";with high_io_queries as + ( + select top $Limit + query_hash, + sum(total_logical_reads + total_logical_writes) io + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_logical_reads + total_logical_writes) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_logical_reads + total_logical_writes as TotalIO, + qs.execution_count as ExecutionCount, + cast((total_logical_reads + total_logical_writes) / (execution_count + 0.0) as money) as AverageIO, + io as QueryTotalIO, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join high_io_queries fq + on fq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by fq.io desc, + fq.query_hash, + qs.total_logical_reads + total_logical_writes desc + option (recompile)" + + $cpu = ";with high_cpu_queries as + ( + select top $Limit + query_hash, + sum(total_worker_time) cpuTime + from sys.dm_exec_query_stats + where query_hash <> 0x0 + group by query_hash + order by sum(total_worker_time) desc + ) + select $instancecolumns + coalesce(db_name(st.dbid), db_name(cast(pa.value AS INT)), 'Resource') AS [Database], + coalesce(object_name(st.objectid, st.dbid), '') as ObjectName, + qs.query_hash as QueryHash, + qs.total_worker_time as CpuTime, + qs.execution_count as ExecutionCount, + cast(total_worker_time / (execution_count + 0.0) as money) as AverageCpuMs, + cpuTime as QueryTotalCpu, + SUBSTRING(st.TEXT,(qs.statement_start_offset + 2) / 2, + (CASE + WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),st.text)) * 2 + ELSE qs.statement_end_offset + END - qs.statement_start_offset) / 2) as QueryText, + qp.query_plan as QueryPlan + from sys.dm_exec_query_stats qs + join high_cpu_queries hcq + on hcq.query_hash = qs.query_hash + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan (qs.plan_handle) qp + outer apply sys.dm_exec_plan_attributes(qs.plan_handle) pa + where pa.attribute = 'dbid' $wheredb $wherenotdb $whereexcludesystem + order by hcq.cpuTime desc, + hcq.query_hash, + qs.total_worker_time desc + option (recompile)" + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + if ($server.ConnectionContext.StatementTimeout -ne 0) { + $server.ConnectionContext.StatementTimeout = 0 + } + + if ($Type -in "All", "Duration") { + try { + Write-Message -Level Debug -Message "Executing SQL: $duration" + $server.Query($duration) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for duration." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "Frequency") { + try { + Write-Message -Level Debug -Message "Executing SQL: $frequency" + $server.Query($frequency) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for frequency." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "IO") { + try { + Write-Message -Level Debug -Message "Executing SQL: $io" + $server.Query($io) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for IO." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($Type -in "All", "CPU") { + try { + Write-Message -Level Debug -Message "Executing SQL: $cpu" + $server.Query($cpu) | Select-DefaultView -ExcludeProperty QueryPlan + } + catch { + Stop-Function -Message "Failure executing query for CPU." -ErrorRecord $_ -Target $server -Continue + } + } + } + } +} diff --git a/functions/Get-DbaTrace.ps1 b/functions/Get-DbaTrace.ps1 new file mode 100644 index 000000000000..7b820810586d --- /dev/null +++ b/functions/Get-DbaTrace.ps1 @@ -0,0 +1,129 @@ +function Get-DbaTrace { + <# + .SYNOPSIS + Gets a list of trace(s) from specified SQL Server Instance + + .DESCRIPTION + This function returns a list of traces on a SQL Server instance and identifies the default trace file + + .PARAMETER SqlInstance + A SQL Server instance to connect to + + .PARAMETER SqlCredential + A credential to use to connect to the SQL Instance rather than using Windows Authentication + + .PARAMETER Id + The id(s) of the Trace + + .PARAMETER Default + Switch that will only return the information for the default system trace + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + + Author: Garry Bargsley (@gbargsley), http://blog.garrybargsley.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2016 + + Lists all the tracefiles on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2016 -Default + + Lists the default trace information on the sql2016 SQL Server. + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int[]]$Id, + [switch]$Default, + [switch][Alias('Silent')] + $EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Get-DbaTraceFile + + # A Microsoft.SqlServer.Management.Trace.TraceServer class exists but is buggy + # and requires x86 PowerShell. So we'll go with T-SQL. + $sql = "SELECT id, status, path, max_size, stop_time, max_files, is_rowset, is_rollover, is_shutdown, is_default, buffer_count, buffer_size, file_position, reader_spid, start_time, last_event_time, event_count, dropped_event_count FROM sys.traces" + + if ($Id) { + $idstring = $Id -join "," + $sql = "$sql WHERE id in ($idstring)" + } + } + process { + foreach ($instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + try { + $results = $server.Query($sql) + } + catch { + Stop-Function -Message "Issue collecting trace data on $server" -Target $server -ErrorRecord $_ + } + + if ($Default) { + $results = $results | Where-Object { $_.is_default } + } + + foreach ($row in $results) { + if ($row.Path.ToString().Length -gt 0) { + $remotefile = Join-AdminUnc -servername $server.NetName -filepath $row.path + } + else { + $remotefile = $null + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Id = $row.id + Status = $row.status + IsRunning = ($row.status -eq 1) + Path = $row.path + RemotePath = $remotefile + MaxSize = $row.max_size + StopTime = $row.stop_time + MaxFiles = $row.max_files + IsRowset = $row.is_rowset + IsRollover = $row.is_rollover + IsShutdown = $row.is_shutdown + IsDefault = $row.is_default + BufferCount = $row.buffer_count + BufferSize = $row.buffer_size + FilePosition = $row.file_position + ReaderSpid = $row.reader_spid + StartTime = $row.start_time + LastEventTime = $row.last_event_time + EventCount = $row.event_count + DroppedEventCount = $row.dropped_event_count + Parent = $server + SqlCredential = $SqlCredential + } | Select-DefaultView -ExcludeProperty Parent, RemotePath, RemoStatus, SqlCredential + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaTraceFlag.ps1 b/functions/Get-DbaTraceFlag.ps1 new file mode 100644 index 000000000000..2748f68cff32 --- /dev/null +++ b/functions/Get-DbaTraceFlag.ps1 @@ -0,0 +1,96 @@ +function Get-DbaTraceFlag { + <# + .SYNOPSIS + Get global Trace Flag(s) information for each instance(s) of SQL Server. + + .DESCRIPTION + Returns Trace Flags that are enabled globally on each instance(s) of SQL Server as an object. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER TraceFlag + Use this switch to filter to a specific Trace Flag. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: TraceFlag + Author: Kevin Bullen (@sqlpadawan) + + References: https://docs.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-traceon-trace-flags-transact-sql + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaTraceFlag + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost + + Returns all Trace Flag information on the local default SQL Server instance + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost, sql2016 + + Returns all Trace Flag(s) for the local and sql2016 SQL Server instances + + .EXAMPLE + Get-DbaTraceFlag -SqlInstance localhost -TraceFlag 4199,3205 + + Returns Trace Flag status for TF 4199 and 3205 for the local SQL Server instance if they are enabled. + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [int[]]$TraceFlag, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $tflags = $server.EnumActiveGlobalTraceFlags() + + if ($tFlags.Rows.Count -eq 0) { + Write-Message -Level Output -Message "No global trace flags enabled" + return + } + + if ($TraceFlag) { + $tflags = $tflags | Where-Object TraceFlag -In $TraceFlag + } + + foreach ($tflag in $tflags) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TraceFlag = $tflag.TraceFlag + Global = $tflag.Global + Session = $tflag.Session + Status = $tflag.Status + } | Select-DefaultView -ExcludeProperty 'Session' + } + } + } +} diff --git a/functions/Get-DbaTrigger.ps1 b/functions/Get-DbaTrigger.ps1 index a8850463b843..6ce7d2bec8a3 100644 --- a/functions/Get-DbaTrigger.ps1 +++ b/functions/Get-DbaTrigger.ps1 @@ -1,28 +1,32 @@ -Function Get-DbaTrigger -{ -<# +function Get-DbaTrigger { + <# .SYNOPSIS Get all existing triggers on one or more SQL instances. .DESCRIPTION Get all existing triggers on one or more SQL instances. -Default output includes columns ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastMofied. +Default output includes columns ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. .PARAMETER SqlInstance The SQL Instance that you're connecting to. -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user. +.PARAMETER SqlCredential +SqlCredential object used to connect to the SQL Server as a different user. + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER ExcludeDatabase +The database(s) to exclude - this list is auto-populated from the server .NOTES +Tags: Database, Triggers Author: Klaas Vandenberghe ( @PowerDBAKlaas ) -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Get-DbaTrigger @@ -30,88 +34,91 @@ You should have received a copy of the GNU General Public License along with thi .EXAMPLE Get-DbaTrigger -SqlInstance ComputerA\sql987 -Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastMofied. +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. .EXAMPLE Get-DbaTrigger -SqlInstance 'ComputerA\sql987','ComputerB' -Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastMofied from two instances. +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified from two instances. .EXAMPLE Get-DbaTrigger -SqlInstance ComputerA\sql987 | Out-Gridview -Returns a gridview displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastMofied. +Returns a gridview displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified. .EXAMPLE 'ComputerA\sql987','ComputerB' | Get-DbaTrigger | Out-Gridview -Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastMofied from two instances. +Returns a custom object displaying ComputerName, SqlInstance, Database, TriggerName, IsEnabled and DateLastModified from two instances. #> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer","instance")] - [string[]]$SqlInstance, - [Alias("SqlCredential")] - [PsCredential]$Credential - ) - - DynamicParam { - if ($SqlInstance) { - return Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $Credential - } - } - - BEGIN {} - - PROCESS { - foreach ($Instance in $SqlInstance) - { + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "instance")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase + ) + + process { + foreach ($Instance in $SqlInstance) { Write-Verbose "Connecting to $Instance" - try - { - $server = Connect-SqlServer -SqlServer $Instance -SqlCredential $Credential -Erroraction SilentlyContinue - } - catch - { - Write-Warning "Can't connect to $Instance" - continue - } + try { + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential -Erroraction SilentlyContinue + } + catch { + Write-Warning "Can't connect to $Instance" + continue + } Write-Verbose "Getting Server Level Triggers on $Instance" - $server.Triggers | - ForEach-Object { - [PSCustomObject]@{ - ComputerName = $server.NetName - SqlInstance = $server.ServiceName - TriggerLevel = "Server" - Database = $null - TriggerName = $_.Name - Status = switch ( $_.IsEnabled ) { $true {"Enabled"} $false {"Disabled"} } - DateLastModified = $_.DateLastModified - } + $server.Triggers | + ForEach-Object { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TriggerLevel = "Server" + Database = $null + TriggerName = $_.Name + Status = switch ($_.IsEnabled) { $true { "Enabled" } $false { "Disabled" } } + DateLastModified = $_.DateLastModified + } } Write-Verbose "Getting Database Level Triggers on $Instance" - $server.Databases | Where-Object { $_.status -eq 'Normal'} | + $dbs = $server.Databases | Where-Object { $_.status -eq 'Normal' } + + if ($Database) { + $dbs = $dbs | Where-Object Name -in $Database + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -notin $ExcludeDatabase + } + + $dbs | ForEach-Object { - $DatabaseName = $_.Name - Write-Verbose "Getting Database Level Triggers on Database $DatabaseName on $Instance" - $_.Triggers | - ForEach-Object { - [PSCustomObject]@{ - ComputerName = $server.NetName - SqlInstance = $server.ServiceName - TriggerLevel = "Database" - Database = $DatabaseName - TriggerName = $_.Name - Status = switch ( $_.IsEnabled ) { $true {"Enabled"} $false {"Disabled"} } - DateLastModified = $_.DateLastModified - } - } + $DatabaseName = $_.Name + Write-Verbose "Getting Database Level Triggers on Database $DatabaseName on $Instance" + $_.Triggers | + ForEach-Object { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + TriggerLevel = "Database" + Database = $DatabaseName + TriggerName = $_.Name + Status = switch ($_.IsEnabled) { $true { "Enabled" } $false { "Disabled" } } + DateLastModified = $_.DateLastModified + } } + } } } - END {} } diff --git a/functions/Get-DbaUptime.ps1 b/functions/Get-DbaUptime.ps1 index e62c4cdb3fc1..13f4933affb9 100644 --- a/functions/Get-DbaUptime.ps1 +++ b/functions/Get-DbaUptime.ps1 @@ -1,187 +1,132 @@ -function Get-DbaUptime -{ -<# -.SYNOPSIS -Returns the uptime of the SQL Server instance, and if required the hosting windows server - -.DESCRIPTION -By default, this command returns for each SQL Server instance passed in: -SQL Instance last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string -Hosting Windows server last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER SqlCredential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER WindowsCredential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER SqlOnly -Excludes the Windows server information - -.PARAMETER WindowsOnly -Excludes the SQL server information - -.NOTES -Tags: CIM -Original Author: Stuart Moore (@napalmgram), stuart-moore.com - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see - -.LINK -https://dbatools.io/Get-DbaUptime - -.EXAMPLE -Get-DbaUptime -SqlServer SqlBox1\Instance2 - -Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on winserver - -.EXAMPLE -Get-DbaUptime -SqlServer winserver\sqlexpress, sql2016 - -Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on host winserver and the default instance on host sql2016 - -.EXAMPLE -Get-DbaUptime -SqlServer sqlserver2014a, sql2016 -SqlOnly - -Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string for the sqlexpress instance on host winserver and the default instance on host sql2016 - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sql2014 | Get-DbaUptime - -Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for every server listed in the Central Management Server on sql2014 - -#> - [CmdletBinding(DefaultParameterSetName = "Default")] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance", "ComputerName")] - [object[]]$SqlServer, - [parameter(ParameterSetName = "Sql")] - [Switch]$SqlOnly, - [parameter(ParameterSetName = "Windows")] - [Switch]$WindowsOnly, - [Alias("Credential")] - [PsCredential]$SqlCredential, - [PsCredential]$WindowsCredential - ) - - PROCESS - { - foreach ($instance in $SqlServer) - { - if ($instance.Gettype().FullName -eq [System.Management.Automation.PSCustomObject] ) - { - $servername = $instance.SqlInstance - } - elseif ($instance.Gettype().FullName -eq [Microsoft.SqlServer.Management.Smo.Server]) - { - $servername = $instance.NetName - } - else - { - $servername = $instance - } - - if ($WindowsOnly -ne $true) - { - - Write-Verbose "Connecting to $servername" - try - { - $server = Connect-SqlServer -SqlServer $servername -SqlCredential $SqlCredential -ErrorVariable ConnectError - - } - catch - { - Write-Warning $_ - continue - } - - Write-Verbose "Getting Start times for $servername" - #Get TempDB creation date - $SQLStartTime = $server.Databases["TempDB"].CreateDate - $SQLUptime = New-TimeSpan -Start $SQLStartTime -End (Get-Date) - $SQLUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($SQLUptime.Days), $($SQLUptime.Hours), $($SQLUptime.Minutes), $($SQLUptime.Seconds) - } - - if ($SqlOnly -ne $true) - { - $WindowsServerName = (Resolve-DbaNetworkName $servername -Credential $WindowsCredential).ComputerName - - try - { - Write-Verbose "Getting WinBootTime via CimInstance for $servername" - $WinBootTime = (Get-CimInstance -ClassName win32_operatingsystem -ComputerName $windowsServerName -ErrorAction SilentlyContinue).lastbootuptime - $WindowsUptime = New-TimeSpan -start $WinBootTime -end (get-date) - $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) - - } - catch - { - try - { - Write-Verbose "$functionname - Getting WinBootTime via CimInstance DCOM" - $CimOption = New-CimSessionOption -Protocol DCOM - $CimSession = New-CimSession -Credential:$WindowsCredential -ComputerName $WindowsServerName -SessionOption $CimOption - $WinBootTime = ($CimSession | Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime - $WindowsUptime = New-TimeSpan -start $WinBootTime -end (get-date) - $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) - - } - catch - { - Write-Exception $_ - } - } - - if ($null -eq $WinBootTime) - { - #Skip the windows results as they'll either be garbage or not there. - $SqlOnly = $true - } - } - - if ($SqlOnly -eq $true) - { - [PSCustomObject]@{ - ComputerName = $server.NetName - InstanceName = $server.ServiceName - SqlServer = $server.Name - SqlUptime = $SQLUptime - SqlStartTime = $SQLStartTime - SinceSqlStart = $SQLUptimeString - } - } - elseif ($WindowsOnly -eq $true) - { - [PSCustomObject]@{ - ComputerName = $WindowsServerName - WindowsUptime = $WindowsUptime - WindowsBootTime = $WinBootTime - SinceWindowsBoot = $WindowsUptimeString - } - } - else - { - [PSCustomObject]@{ - ComputerName = $WindowsServerName - InstanceName = $server.ServiceName - SqlServer = $server.Name - SqlUptime = $SQLUptime - WindowsUptime = $WindowsUptime - SqlStartTime = $SQLStartTime - WindowsBootTime = $WinBootTime - SinceSqlStart = $SQLUptimeString - SinceWindowsBoot = $WindowsUptimeString - } - } - } - } +function Get-DbaUptime { + <# + .SYNOPSIS + Returns the uptime of the SQL Server instance, and if required the hosting windows server + + .DESCRIPTION + By default, this command returns for each SQL Server instance passed in: + SQL Instance last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string + Hosting Windows server last startup time, Uptime as a PS TimeSpan, Uptime as a formatted string + + .PARAMETER SqlInstance + The SQL Server instance that you're connecting to. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). To use: + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. + + To connect to SQL Server as a different Windows user, run PowerShell as that user. + + .PARAMETER Credential + Allows you to login to the computer (not SQL Server instance) using alternative Windows credentials. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: CIM + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaUptime + + .EXAMPLE + Get-DbaUptime -SqlInstance SqlBox1\Instance2 + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on winserver + + .EXAMPLE + Get-DbaUptime -SqlInstance winserver\sqlexpress, sql2016 + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for the sqlexpress instance on host winserver and the default instance on host sql2016 + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Get-DbaUptime + + Returns an object with SQL Server start time, uptime as TimeSpan object, uptime as a string, and Windows host boot time, host uptime as TimeSpan objects and host uptime as a string for every server listed in the Central Management Server on sql2014 + + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $nowutc = (Get-Date).ToUniversalTime() + } + process { + foreach ($instance in $SqlInstance) { + if ($instance.Gettype().FullName -eq [System.Management.Automation.PSCustomObject] ) { + $servername = $instance.SqlInstance + } + elseif ($instance.Gettype().FullName -eq [Microsoft.SqlServer.Management.Smo.Server]) { + $servername = $instance.NetName + } + else { + $servername = $instance.ComputerName; + } + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + Write-Message -Level Verbose -Message "Getting start times for $servername" + #Get tempdb creation date + [dbadatetime]$SQLStartTime = $server.Databases["tempdb"].CreateDate + $SQLUptime = New-TimeSpan -Start $SQLStartTime.ToUniversalTime() -End $nowutc + $SQLUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($SQLUptime.Days), $($SQLUptime.Hours), $($SQLUptime.Minutes), $($SQLUptime.Seconds) + + $WindowsServerName = (Resolve-DbaNetworkName $servername -Credential $Credential).FullComputerName + + try { + Write-Message -Level Verbose -Message "Getting WinBootTime via CimInstance for $servername" + $WinBootTime = (Get-DbaOperatingSystem -ComputerName $windowsServerName -Credential $Credential -ErrorAction SilentlyContinue).LastBootTime + $WindowsUptime = New-TimeSpan -start $WinBootTime.ToUniversalTime() -end $nowutc + $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) + } + catch { + try { + Write-Message -Level Verbose -Message "Getting WinBootTime via CimInstance DCOM" + $CimOption = New-CimSessionOption -Protocol DCOM + $CimSession = New-CimSession -Credential:$Credential -ComputerName $WindowsServerName -SessionOption $CimOption + [dbadatetime]$WinBootTime = ($CimSession | Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime + $WindowsUptime = New-TimeSpan -start $WinBootTime.ToUniversalTime() -end $nowutc + $WindowsUptimeString = "{0} days {1} hours {2} minutes {3} seconds" -f $($WindowsUptime.Days), $($WindowsUptime.Hours), $($WindowsUptime.Minutes), $($WindowsUptime.Seconds) + } + catch { + Stop-Function -Message "Failure getting WinBootTime" -ErrorRecord $_ -Target $instance -Continue + } + } + + [PSCustomObject]@{ + ComputerName = $WindowsServerName + InstanceName = $server.ServiceName + SqlServer = $server.Name + SqlUptime = $SQLUptime + WindowsUptime = $WindowsUptime + SqlStartTime = $SQLStartTime + WindowsBootTime = $WinBootTime + SinceSqlStart = $SQLUptimeString + SinceWindowsBoot = $WindowsUptimeString + } + } + } } diff --git a/functions/Get-DbaUserLevelPermission.ps1 b/functions/Get-DbaUserLevelPermission.ps1 new file mode 100644 index 000000000000..254a10204f86 --- /dev/null +++ b/functions/Get-DbaUserLevelPermission.ps1 @@ -0,0 +1,283 @@ +function Get-DbaUserLevelPermission { + <# + .SYNOPSIS + Displays detailed permissions information for the server and database roles and securables. + + .DESCRIPTION + This command will display all server logins, server level securable, database logins and database securables. + + DISA STIG implementators will find this command useful as it uses Permissions.sql provided by DISA. + + Note that if you Ctrl-C out of this command and end it prematurely, it will leave behind a STIG schema in tempdb. + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER ExcludeSystemDatabase + Allows you to suppress output on system databases + + .PARAMETER IncludePublicGuest + Allows you to include output for public and guest grants. + + .PARAMETER IncludeSystemObjects + Allows you to include output on sys schema objects. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Discovery, Permissions, Security + Author: Brandon Abshire, netnerds.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaUserLevelPermission + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008, sqlserver2012 + Check server and database permissions for servers sql2008 and sqlserver2012. + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008 -Database TestDB + Check server and database permissions on server sql2008 for only the TestDB database + + .EXAMPLE + Get-DbaUserLevelPermission -SqlInstance sql2008 -Database TestDB -IncludePublicGuest -IncludeSystemObjects + Check server and database permissions on server sql2008 for only the TestDB database, + including public and guest grants, and sys schema objects. + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Position = 1, Mandatory = $false)] + [switch]$ExcludeSystemDatabase, + [switch]$IncludePublicGuest, + [switch]$IncludeSystemObjects, + [Alias('Silent')] + [switch]$EnableException + ) + + BEGIN { + + $sql = [System.IO.File]::ReadAllText("$script:PSModuleRoot\bin\stig.sql") + + $endSQL = " BEGIN TRY DROP FUNCTION STIG.server_effective_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.server_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.members_of_server_role END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.server_roles_of END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.server_role_members END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.database_effective_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.database_permissions END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.members_of_db_role END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP FUNCTION STIG.database_roles_of END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP VIEW STIG.database_role_members END TRY BEGIN CATCH END CATCH; + GO + BEGIN TRY DROP SCHEMA STIG END TRY BEGIN CATCH END CATCH; + GO" + + + $serverSQL = "SELECT 'SERVER LOGINS' AS Type , + sl.name AS Member , + ISNULL(srm.role, 'None') AS [Role/Securable/Class] , + ' ' AS [Schema/Owner] , + ' ' AS [Securable] , + ' ' AS [Grantee Type] , + ' ' AS [Grantee] , + ' ' AS [Permission] , + ' ' AS [State] , + ' ' AS [Grantor] , + ' ' AS [Grantor Type] , + ' ' AS [Source View] + FROM master.sys.syslogins sl + LEFT JOIN tempdb.[STIG].[server_role_members] srm ON sl.name = srm.member + WHERE sl.name NOT LIKE 'NT %' + AND sl.name NOT LIKE '##%' + UNION + SELECT 'SERVER SECURABLES' AS Type , + sl.name , + sp.[Securable Class] COLLATE SQL_Latin1_General_CP1_CI_AS , + ' ' , + sp.[Securable] , + sp.[Grantee Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.Grantee , + sp.Permission COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.State COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.Grantor , + sp.[Grantor Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + sp.[Source View] + FROM master.sys.syslogins sl + LEFT JOIN tempdb.[STIG].[server_permissions] sp ON sl.name = sp.Grantee + WHERE sl.name NOT LIKE 'NT %' + AND sl.name NOT LIKE '##%';" + + $dbSQL = "SELECT 'DB ROLE MEMBERS' AS type , + Member , + Role , + ' ' AS [Schema/Owner] , + ' ' AS [Securable] , + ' ' AS [Grantee Type] , + ' ' AS [Grantee] , + ' ' AS [Permission] , + ' ' AS [State] , + ' ' AS [Grantor] , + ' ' AS [Grantor Type] , + ' ' AS [Source View] + FROM tempdb.[STIG].[database_role_members] + UNION + SELECT DISTINCT + 'DB SECURABLES' AS Type , + drm.member , + dp.[Securable Type or Class] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.[Schema/Owner] , + dp.Securable , + dp.[Grantee Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.Grantee , + dp.Permission COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.State COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.Grantor , + dp.[Grantor Type] COLLATE SQL_Latin1_General_CP1_CI_AS , + dp.[Source View] + FROM tempdb.[STIG].[database_role_members] drm + LEFT JOIN tempdb.[STIG].[database_permissions] dp ON ( drm.member = dp.grantee + OR drm.role = dp.grantee + ) + WHERE dp.Grantor IS NOT NULL + AND [Schema/Owner] <> 'sys'" + + if ($IncludePublicGuest) { $dbSQL = $dbSQL.Replace("LEFT JOIN", "FULL JOIN") } + if ($IncludeSystemObjects) { $dbSQL = $dbSQL.Replace("AND [Schema/Owner] <> 'sys'", "") } + + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $dbs = $server.Databases + + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($ExcludeSystemDatabase) { + $dbs = $dbs | Where-Object IsSystemObject -eq $false + } + + #reset $serverDT + $serverDT = $null + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsAccessible -eq $false) { + Stop-Function -Message "The database $db is not accessible" -Continue + } + + $sql = $sql.Replace("", $db.Name) + + #Create objects in active database + Write-Message -Level Verbose -Message "Creating objects" + try { $db.ExecuteNonQuery($sql) } catch {} # sometimes it complains about not being able to drop the stig schema if the person Ctrl-C'd before. + + #Grab permissions data + if (-not $serverDT) { + Write-Message -Level Verbose -Message "Building data table for server objects" + + try { $serverDT = $db.Query($serverSQL) } catch { } + + foreach ($row in $serverDT) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Object = 'SERVER' + Type = $row.Type + Member = $row.Member + RoleSecurableClass = $row.'Role/Securable/Class' + SchemaOwner = $row.'Schema/Owner' + Securable = $row.Securable + GranteeType = $row.'Grantee Type' + Grantee = $row.Grantee + Permission = $row.Permission + State = $row.State + Grantor = $row.Grantor + GrantorType = $row.'Grantor Type' + SourceView = $row.'Source View' + } + } + } + + Write-Message -Level Verbose -Message "Building data table for $db objects" + try { $dbDT = $db.Query($dbSQL) } catch { } + + foreach ($row in $dbDT) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Object = $db.Name + Type = $row.Type + Member = $row.Member + RoleSecurableClass = $row.'Role/Securable/Class' + SchemaOwner = $row.'Schema/Owner' + Securable = $row.Securable + GranteeType = $row.'Grantee Type' + Grantee = $row.Grantee + Permission = $row.Permission + State = $row.State + Grantor = $row.Grantor + GrantorType = $row.'Grantor Type' + SourceView = $row.'Source View' + } + } + + #Delete objects + Write-Message -Level Verbose -Message "Deleting objects" + try { $db.ExecuteNonQuery($endSQL) } catch { } + $sql = $sql.Replace($db.Name, "") + + #Sashay Away + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaWaitResource.ps1 b/functions/Get-DbaWaitResource.ps1 new file mode 100644 index 000000000000..bb608b6428bf --- /dev/null +++ b/functions/Get-DbaWaitResource.ps1 @@ -0,0 +1,164 @@ +function Get-DbaWaitResource { + <# + .SYNOPSIS + Returns the resource being waited upon + + .DESCRIPTION + Given a wait resource in the form of: + 'PAGE: 10:1:9180084 ' + returns the database, data file and the system object which is being waited up. + Given a wait resource in the form of: + 'KEY: 7:35457594073541168 (de21f92a1572)' + returns the database, object and index that is being waited on, With the -row switch the row data will also be returned. + .PARAMETER SqlInstance + The SQL Server instance to restore to. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + + .PARAMETER WaitResource + The waitresource value as supplied in sys.dm_exec_requests + + .PARAMETER Row + If this switch provided also returns the value of the row being waited on with KEY wait resources + + .PARAMETER EnableException + Replaces user friendly yellow warnings with bloody red exceptions of doom! + Use this if you want the function to throw terminating errors you want to catch. + + .EXAMPLE + Get-DbaWaitResource -SqlInstance server1 -WaitResource 'PAGE: 10:1:9180084' + + Will return an object containing; database name, data file name, schema name and the object which owns the resource + + .EXAMPLE + Get-DbaWaitResource -Sql Instance server2 -WaitResource 'KEY: 7:35457594073541168 (de21f92a1572)' + + Will return an object containing; database name, schema name and index name which is being waited on. + + .EXAMPLE + Get-DbaWaitResource -Sql Instance server2 -WaitResource 'KEY: 7:35457594073541168 (de21f92a1572)' -row + + Will return an object containing; database name, schema name and index name which is being waited on, and in addition the contents of the locked row at the time the command is run. + + .NOTES + Tags: Pages, DBCC + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$WaitResource, + [switch]$Row, + [switch]$EnableException + ) + + process { + if ($WaitResource -notmatch '^PAGE: [0-9]*:[0-9]*:[0-9]*$' -and $WaitResource -notmatch '^KEY: [0-9]*:[0-9]* \([a-f0-9]*\)$'){ + Stop-Function -Message "Row input - $WaitResource - Improperly formatted" + return + } + + try { + $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Cannot connect to $SqlInstance" + } + + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):*' + $ResourceType = $matches.Type + $DbId = $matches.DbId + $DbName = ($server.Databases | Where-Object ID -eq $dbid).Name + if ($null -eq $DbName){ + stop-function -Message "Database with id $dbid does not exist on $server" + return + } + if ($ResourceType -eq 'PAGE'){ + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):(?[0-9]*):(?[0-9]*)$' + $DataFileSql = "select name, physical_name from sys.master_files where database_id=$DbID and file_ID=$($matches.FileID);" + $DataFile = $server.query($DataFileSql) + if ($null -eq $DataFile){ + Write-Message -Level Warning -Message "Datafile with id $($matches.FileID) for $dbname not found" + return + } + $ObjectIdSQL = "dbcc traceon (3604); dbcc page ($dbid,$($matches.fileID),$($matches.PageID),2) with tableresults;" + try { + $ObjectID = ($server.databases[$dbname].Query($ObjectIdSQL) | Where-Object Field -eq 'Metadata: ObjectId').Value + } + catch { + Stop-Function -Message "You've requested a page beyond the end of the database, exiting" + return + } + if ($null -eq $ObjectID){ + Write-Message -Level Warning -Message "Object not found, could have been delete, or a transcription error when copying the Wait_resource to PowerShell" + return + } + $ObjectSql = "select SCHEMA_NAME(schema_id) as SchemaName, name, type_desc from sys.all_objects where object_id=$objectID;" + $Object = $server.databases[$dbname].query($ObjectSql) + if ($null -eq $Object){ + Write-Message -Warning "Object could not be found. Could have been removed, or could be a transcription error copying the Wait_resource to sowerShell" + } + [PsCustomObject]@{ + DatabaseID = $DbId + DatabaseName = $DbName + DataFileName = $Datafile.name + DataFilePath = $DataFile.physical_name + ObjectID = $ObjectID + ObjectName = $Object.Name + ObjectSchema = $Object.SchemaName + ObjectType = $Object.type_desc + } + } + if ($ResourceType -eq 'KEY'){ + $null = $WaitResource -match '^(?[A-Z]*): (?[0-9]*):(?[0-9]*) (?\(.*\))$' + $IndexSql = "select + sp.object_id as ObjectID, + OBJECT_SCHEMA_NAME(sp.object_id) as SchemaName, + sao.name as ObjectName, + si.name as IndexName + from + sys.partitions sp inner join sys.indexes si on sp.index_id=si.index_id and sp.object_id=si.object_id + inner join sys.all_objects sao on sp.object_id=sao.object_id + where + hobt_id = $($matches.frodo); + " + $Index = $server.databases[$dbname].Query($IndexSql) + if ($null -eq $Index){ + Write-Message -Level Warning -Message "Heap or B-Tree with ID $($matches.frodo) can not be found in $dbname on $server" + return + } + $output = [PsCustomObject]@{ + DatabaseID = $DbId + DatabaseName = $DbName + SchemaName = $Index.SchemaName + IndexName = $Index.IndexName + ObjectID = $index.ObjectID + Objectname = $index.ObjectName + HobtID = $matches.frodo + } + if ($row -eq $True){ + $DataSql = "select * from $($Index.SchemaName).$($Index.ObjectName) with (NOLOCK) where %%lockres%% ='$($matches.physloc)'" + $Data = $server.databases[$dbname].query($DataSql) + if ($null -eq $data){ + Write-Message -Level warning -Message "Could not retrieve the data. It may have been deleted or moved since the wait resource value was generated" + } + else{ + $output | Add-Member -Type NoteProperty -Name ObjectData -Value $Data + $output | Select-Object * -ExpandProperty ObjectData + } + } + else { + $output + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaWaitStatistic.ps1 b/functions/Get-DbaWaitStatistic.ps1 new file mode 100644 index 000000000000..ba5a7209f818 --- /dev/null +++ b/functions/Get-DbaWaitStatistic.ps1 @@ -0,0 +1,907 @@ +function Get-DbaWaitStatistic { + <# + .SYNOPSIS + Displays wait statistics + + .DESCRIPTION + This command is based off of Paul Randal's post "Wait statistics, or please tell me where it hurts" + + Returns: + WaitType + Category + WaitSeconds + ResourceSeconds + SignalSeconds + WaitCount + Percentage + AverageWaitSeconds + AverageResourceSeconds + AverageSignalSeconds + URL + + Reference: https://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/ + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Threshold + Threshold, in percentage of all waits on the system. Default per Paul's post is 95%. + + .PARAMETER IncludeIgnorable + Some waits are no big deal and can be safely ignored in most circumstances. If you've got weird issues with mirroring or AGs. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: WaitStatistic + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWaitStatistic + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008, sqlserver2012 + + Check wait statistics for servers sql2008 and sqlserver2012 + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008 -Threshold 98 -IncludeIgnorable + + Check wait statistics on server sql2008 for thresholds above 98% and include wait stats that are most often, but not always, ignorable + + .EXAMPLE + Get-DbaWaitStatistic -SqlInstance sql2008 | Select * + + Shows detailed notes, if available, from Paul's post + + .EXAMPLE + $output = Get-DbaWaitStatistic -SqlInstance sql2008 -Threshold 100 -IncludeIgnorable | Select * | ConvertTo-DbaDataTable + + Collects all Wait Statistics (including ignorable waits) on server sql2008 into a Data Table. + + + .EXAMPLE + $output = Get-DbaWaitStatistic -SqlInstance sql2008 + $output + foreach ($row in ($output | Sort-Object -Unique Url)) { Start-Process ($row).Url } + + Displays the output then loads the associated sqlskills website for each result. Opens one tab per unique URL. + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int]$Threshold = 95, + [switch]$IncludeIgnorable, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $details = [pscustomobject]@{ + CXPACKET = "This indicates parallelism, not necessarily that there's a problem. The coordinator thread in a parallel query always accumulates these waits. If the parallel threads are not given equal amounts of work to do, or one thread blocks, the waiting threads will also accumulate CXPACKET waits, which will make them aggregate a lot faster - this is a problem. One thread may have a lot more to do than the others, and so the whole query is blocked while the long-running thread completes. If this is combined with a high number of PAGEIOLATCH_XX waits, it could be large parallel table scans going on because of incorrect non-clustered indexes, or a bad query plan. If neither of these are the issue, you might want to try setting MAXDOP to 4, 2, or 1 for the offending queries (or possibly the whole instance). Make sure that if you have a NUMA system that you try setting MAXDOP to the number of cores in a single NUMA node first to see if that helps the problem. You also need to consider the MAXDOP effect on a mixed-load system. Play with the cost threshold for parallelism setting (bump it up to, say, 25) before reducing the MAXDOP of the whole instance. And don't forget Resource Governor in Enterprise Edition of SQL Server 2008 onward that allows DOP governing for a particular group of connections to the server." + PAGEIOLATCH_XX = "This is where SQL Server is waiting for a data page to be read from disk into memory. It may indicate a bottleneck at the IO subsystem level (which is a common knee-jerk response to seeing these), but why is the I/O subsystem having to service so many reads? It could be buffer pool/memory pressure (i.e. not enough memory for the workload), a sudden change in query plans causing a large parallel scan instead of a seek, plan cache bloat, or a number of other things. Don't assume the root cause is the I/O subsystem." + ASYNC_NETWORK_IO = "This is usually where SQL Server is waiting for a client to finish consuming data. It could be that the client has asked for a very large amount of data or just that it's consuming it reeeeeally slowly because of poor programming – I rarely see this being a network issue. Clients often process one row at a time – called RBAR or Row-By-Agonizing-Row – instead of caching the data on the client and acknowledging to SQL Server immediately." + WRITELOG = "This is the log management system waiting for a log flush to disk. It commonly indicates that the I/O subsystem can't keep up with the log flush volume, but on very high-volume systems it could also be caused by internal log flush limits, that may mean you have to split your workload over multiple databases or even make your transactions a little longer to reduce log flushes. To be sure it is the I/O subsystem, use the DMV sys.dm_io_virtual_file_stats to examine the I/O latency for the log file and see if it correlates to the average WRITELOG time. If WRITELOG is longer, you've got internal contention and need to shard. If not, investigate why you're creating so much transaction log." + BROKER_RECEIVE_WAITFOR = "This is just Service Broker waiting around for new messages to receive. I would add this to the list of waits to filter out and re-run the wait stats query." + MSQL_XP = "This is SQL Server waiting for an extended stored-proc to finish. This could indicate a problem in your XP code." + OLEDB = "As its name suggests, this is a wait for something communicating using OLEDB – e.g. a linked server. However, OLEDB is also used by all DMVs and by DBCC CHECKDB, so don't assume linked servers are the problem – it could be a third-party monitoring tool making excessive DMV calls. If it *is* a linked server (wait times in the 10s or 100s of milliseconds), go to the linked server and do wait stats analysis there to figure out what the performance issue is there." + BACKUPIO = "This can show up when you're backing up to a slow I/O subsystem, like directly to tape, which is slooooow, or over a network." + LCK_M_XX = "This is simply the thread waiting for a lock to be granted and indicates blocking problems. These could be caused by unwanted lock escalation or bad programming, but could also be from I/Os taking a long time causing locks to be held for longer than usual. Look at the resource associated with the lock using the DMV sys.dm_os_waiting_tasks. Don't assume that locking is the root cause." + ONDEMAND_TASK_QUEUE = "This is normal and is part of the background task system (e.g. deferred drop, ghost cleanup). I would add this to the list of waits to filter out and re-run the wait stats query." + BACKUPBUFFER = "This commonly show up with BACKUPIO and is a backup thread waiting for a buffer to write backup data into." + IO_COMPLETION = "This is SQL Server waiting for non-data page I/Os to complete and could be an indication that the I/O subsystem is overloaded if the latencies look high (see Are I/O latencies killing your performance?)" + SOS_SCHEDULER_YIELD = "This is code running that doesn't hit any resource waits." + DBMIRROR_EVENTS_QUEUE = "These two are database mirroring just sitting around waiting for something to do. I would add these to the list of waits to filter out and re-run the wait stats query." + DBMIRRORING_CMD = "These two are database mirroring just sitting around waiting for something to do. I would add these to the list of waits to filter out and re-run the wait stats query." + PAGELATCH_XX = "This is contention for access to in-memory copies of pages. The most well-known cases of these are the PFS and SGAM contention that can occur in tempdb under certain workloads. To find out what page the contention is on, you'll need to use the DMV sys.dm_os_waiting_tasks to figure out what page the latch is for. For tempdb issues, Robert Davis (blog | twitter) has a good post showing how to do this. Another common cause I've seen is an index hot-spot with concurrent inserts into an index with an identity value key." + LATCH_XX = "This is contention for some non-page structure inside SQL Server – so not related to I/O or data at all. These can be hard to figure out and you're going to be using the DMV sys.dm_os_latch_stats. More on this in my Latches category." + PREEMPTIVE_OS_PIPEOPS = "This is SQL Server switching to preemptive scheduling mode to call out to Windows for something, and this particular wait is usually from using xp_cmdshell. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + THREADPOOL = "This says that there aren't enough worker threads on the system to satisfy demand. Commonly this is large numbers of high-DOP queries trying to execute and taking all the threads from the thread pool." + BROKER_TRANSMITTER = "This is just Service Broker waiting around for new messages to send. I would add this to the list of waits to filter out and re-run the wait stats query." + SQLTRACE_WAIT_ENTRIES = "Part of SQL Trace. I would add this to the list of waits to filter out and re-run the wait stats query." + DBMIRROR_DBM_MUTEX = "This one is undocumented and is contention for the send buffer that database mirroring shares between all the mirroring sessions on a server. It could indicate that you've got too many mirroring sessions." + RESOURCE_SEMAPHORE = "This is queries waiting for execution memory (the memory used to process the query operators – like a sort). This could be memory pressure or a very high concurrent workload." + PREEMPTIVE_OS_AUTHENTICATIONOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_GENERICOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + SLEEP_BPOOL_FLUSH = "This is normal to see and indicates that checkpoint is throttling itself to avoid overloading the IO subsystem. I would add this to the list of waits to filter out and re-run the wait stats query." + MSQL_DQ = "This is SQL Server waiting for a distributed query to finish. This could indicate a problem with the distributed query, or it could just be normal." + RESOURCE_SEMAPHORE_QUERY_COMPILE = "When there are too many concurrent query compilations going on, SQL Server will throttle them. I don't remember the threshold, but this can indicate excessive recompilation, or maybe single-use plans." + DAC_INIT = "This is the Dedicated Admin Connection initializing." + MSSEARCH = "This is normal to see for full-text operations. If this is the highest wait, it could mean your system is spending most of its time doing full-text queries. You might want to consider adding this to the filter list." + PREEMPTIVE_OS_FILEOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_LIBRARYOPS = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_LOOKUPACCOUNTSID = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + PREEMPTIVE_OS_QUERYREGISTRY = "These are SQL Server switching to preemptive scheduling mode to call out to Windows for something. These were added for 2008 and aren't documented anywhere except through the links to my waits library." + SQLTRACE_LOCK = "Part of SQL Trace. I would add this to the list of waits to filter out and re-run the wait stats query." + } + + # Thanks Brentg Ozar via https://gist.github.com/BrentOzar/42e82ee0603a1917c17d74c3fca26d34 + # Thanks Marcin Gminski‏ via https://www.dropbox.com/s/x3zr7u18tc1ojey/WaitStats.sql?dl=0 + + $category = [pscustomobject]@{ + ASYNC_IO_COMPLETION = 'Other Disk IO' + ASYNC_NETWORK_IO = 'Network IO' + BACKUPIO = 'Other Disk IO' + BROKER_CONNECTION_RECEIVE_TASK = 'Service Broker' + BROKER_DISPATCHER = 'Service Broker' + BROKER_ENDPOINT_STATE_MUTEX = 'Service Broker' + BROKER_EVENTHANDLER = 'Service Broker' + BROKER_FORWARDER = 'Service Broker' + BROKER_INIT = 'Service Broker' + BROKER_MASTERSTART = 'Service Broker' + BROKER_RECEIVE_WAITFOR = 'User Wait' + BROKER_REGISTERALLENDPOINTS = 'Service Broker' + BROKER_SERVICE = 'Service Broker' + BROKER_SHUTDOWN = 'Service Broker' + BROKER_START = 'Service Broker' + BROKER_TASK_SHUTDOWN = 'Service Broker' + BROKER_TASK_STOP = 'Service Broker' + BROKER_TASK_SUBMIT = 'Service Broker' + BROKER_TO_FLUSH = 'Service Broker' + BROKER_TRANSMISSION_OBJECT = 'Service Broker' + BROKER_TRANSMISSION_TABLE = 'Service Broker' + BROKER_TRANSMISSION_WORK = 'Service Broker' + BROKER_TRANSMITTER = 'Service Broker' + CHECKPOINT_QUEUE = 'Idle' + CHKPT = 'Tran Log IO' + CLR_AUTO_EVENT = 'SQL CLR' + CLR_CRST = 'SQL CLR' + CLR_JOIN = 'SQL CLR' + CLR_MANUAL_EVENT = 'SQL CLR' + CLR_MEMORY_SPY = 'SQL CLR' + CLR_MONITOR = 'SQL CLR' + CLR_RWLOCK_READER = 'SQL CLR' + CLR_RWLOCK_WRITER = 'SQL CLR' + CLR_SEMAPHORE = 'SQL CLR' + CLR_TASK_START = 'SQL CLR' + CLRHOST_STATE_ACCESS = 'SQL CLR' + CMEMPARTITIONED = 'Memory' + CMEMTHREAD = 'Memory' + CXPACKET = 'Parallelism' + DBMIRROR_DBM_EVENT = 'Mirroring' + DBMIRROR_DBM_MUTEX = 'Mirroring' + DBMIRROR_EVENTS_QUEUE = 'Mirroring' + DBMIRROR_SEND = 'Mirroring' + DBMIRROR_WORKER_QUEUE = 'Mirroring' + DBMIRRORING_CMD = 'Mirroring' + DTC = 'Transaction' + DTC_ABORT_REQUEST = 'Transaction' + DTC_RESOLVE = 'Transaction' + DTC_STATE = 'Transaction' + DTC_TMDOWN_REQUEST = 'Transaction' + DTC_WAITFOR_OUTCOME = 'Transaction' + DTCNEW_ENLIST = 'Transaction' + DTCNEW_PREPARE = 'Transaction' + DTCNEW_RECOVERY = 'Transaction' + DTCNEW_TM = 'Transaction' + DTCNEW_TRANSACTION_ENLISTMENT = 'Transaction' + DTCPNTSYNC = 'Transaction' + EE_PMOLOCK = 'Memory' + EXCHANGE = 'Parallelism' + EXTERNAL_SCRIPT_NETWORK_IOF = 'Network IO' + FCB_REPLICA_READ = 'Replication' + FCB_REPLICA_WRITE = 'Replication' + FT_COMPROWSET_RWLOCK = 'Full Text Search' + FT_IFTS_RWLOCK = 'Full Text Search' + FT_IFTS_SCHEDULER_IDLE_WAIT = 'Idle' + FT_IFTSHC_MUTEX = 'Full Text Search' + FT_IFTSISM_MUTEX = 'Full Text Search' + FT_MASTER_MERGE = 'Full Text Search' + FT_MASTER_MERGE_COORDINATOR = 'Full Text Search' + FT_METADATA_MUTEX = 'Full Text Search' + FT_PROPERTYLIST_CACHE = 'Full Text Search' + FT_RESTART_CRAWL = 'Full Text Search' + 'FULLTEXT GATHERER' = 'Full Text Search' + HADR_AG_MUTEX = 'Replication' + HADR_AR_CRITICAL_SECTION_ENTRY = 'Replication' + HADR_AR_MANAGER_MUTEX = 'Replication' + HADR_AR_UNLOAD_COMPLETED = 'Replication' + HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST = 'Replication' + HADR_BACKUP_BULK_LOCK = 'Replication' + HADR_BACKUP_QUEUE = 'Replication' + HADR_CLUSAPI_CALL = 'Replication' + HADR_COMPRESSED_CACHE_SYNC = 'Replication' + HADR_CONNECTIVITY_INFO = 'Replication' + HADR_DATABASE_FLOW_CONTROL = 'Replication' + HADR_DATABASE_VERSIONING_STATE = 'Replication' + HADR_DATABASE_WAIT_FOR_RECOVERY = 'Replication' + HADR_DATABASE_WAIT_FOR_RESTART = 'Replication' + HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING = 'Replication' + HADR_DB_COMMAND = 'Replication' + HADR_DB_OP_COMPLETION_SYNC = 'Replication' + HADR_DB_OP_START_SYNC = 'Replication' + HADR_DBR_SUBSCRIBER = 'Replication' + HADR_DBR_SUBSCRIBER_FILTER_LIST = 'Replication' + HADR_DBSEEDING = 'Replication' + HADR_DBSEEDING_LIST = 'Replication' + HADR_DBSTATECHANGE_SYNC = 'Replication' + HADR_FABRIC_CALLBACK = 'Replication' + HADR_FILESTREAM_BLOCK_FLUSH = 'Replication' + HADR_FILESTREAM_FILE_CLOSE = 'Replication' + HADR_FILESTREAM_FILE_REQUEST = 'Replication' + HADR_FILESTREAM_IOMGR = 'Replication' + HADR_FILESTREAM_IOMGR_IOCOMPLETION = 'Replication' + HADR_FILESTREAM_MANAGER = 'Replication' + HADR_FILESTREAM_PREPROC = 'Replication' + HADR_GROUP_COMMIT = 'Replication' + HADR_LOGCAPTURE_SYNC = 'Replication' + HADR_LOGCAPTURE_WAIT = 'Replication' + HADR_LOGPROGRESS_SYNC = 'Replication' + HADR_NOTIFICATION_DEQUEUE = 'Replication' + HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS = 'Replication' + HADR_NOTIFICATION_WORKER_STARTUP_SYNC = 'Replication' + HADR_NOTIFICATION_WORKER_TERMINATION_SYNC = 'Replication' + HADR_PARTNER_SYNC = 'Replication' + HADR_READ_ALL_NETWORKS = 'Replication' + HADR_RECOVERY_WAIT_FOR_CONNECTION = 'Replication' + HADR_RECOVERY_WAIT_FOR_UNDO = 'Replication' + HADR_REPLICAINFO_SYNC = 'Replication' + HADR_SEEDING_CANCELLATION = 'Replication' + HADR_SEEDING_FILE_LIST = 'Replication' + HADR_SEEDING_LIMIT_BACKUPS = 'Replication' + HADR_SEEDING_SYNC_COMPLETION = 'Replication' + HADR_SEEDING_TIMEOUT_TASK = 'Replication' + HADR_SEEDING_WAIT_FOR_COMPLETION = 'Replication' + HADR_SYNC_COMMIT = 'Replication' + HADR_SYNCHRONIZING_THROTTLE = 'Replication' + HADR_TDS_LISTENER_SYNC = 'Replication' + HADR_TDS_LISTENER_SYNC_PROCESSING = 'Replication' + HADR_THROTTLE_LOG_RATE_GOVERNOR = 'Log Rate Governor' + HADR_TIMER_TASK = 'Replication' + HADR_TRANSPORT_DBRLIST = 'Replication' + HADR_TRANSPORT_FLOW_CONTROL = 'Replication' + HADR_TRANSPORT_SESSION = 'Replication' + HADR_WORK_POOL = 'Replication' + HADR_WORK_QUEUE = 'Replication' + HADR_XRF_STACK_ACCESS = 'Replication' + INSTANCE_LOG_RATE_GOVERNOR = 'Log Rate Governor' + IO_COMPLETION = 'Other Disk IO' + IO_QUEUE_LIMIT = 'Other Disk IO' + IO_RETRY = 'Other Disk IO' + LATCH_DT = 'Latch' + LATCH_EX = 'Latch' + LATCH_KP = 'Latch' + LATCH_NL = 'Latch' + LATCH_SH = 'Latch' + LATCH_UP = 'Latch' + LAZYWRITER_SLEEP = 'Idle' + LCK_M_BU = 'Lock' + LCK_M_BU_ABORT_BLOCKERS = 'Lock' + LCK_M_BU_LOW_PRIORITY = 'Lock' + LCK_M_IS = 'Lock' + LCK_M_IS_ABORT_BLOCKERS = 'Lock' + LCK_M_IS_LOW_PRIORITY = 'Lock' + LCK_M_IU = 'Lock' + LCK_M_IU_ABORT_BLOCKERS = 'Lock' + LCK_M_IU_LOW_PRIORITY = 'Lock' + LCK_M_IX = 'Lock' + LCK_M_IX_ABORT_BLOCKERS = 'Lock' + LCK_M_IX_LOW_PRIORITY = 'Lock' + LCK_M_RIn_NL = 'Lock' + LCK_M_RIn_NL_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_NL_LOW_PRIORITY = 'Lock' + LCK_M_RIn_S = 'Lock' + LCK_M_RIn_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_S_LOW_PRIORITY = 'Lock' + LCK_M_RIn_U = 'Lock' + LCK_M_RIn_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_U_LOW_PRIORITY = 'Lock' + LCK_M_RIn_X = 'Lock' + LCK_M_RIn_X_ABORT_BLOCKERS = 'Lock' + LCK_M_RIn_X_LOW_PRIORITY = 'Lock' + LCK_M_RS_S = 'Lock' + LCK_M_RS_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RS_S_LOW_PRIORITY = 'Lock' + LCK_M_RS_U = 'Lock' + LCK_M_RS_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RS_U_LOW_PRIORITY = 'Lock' + LCK_M_RX_S = 'Lock' + LCK_M_RX_S_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_S_LOW_PRIORITY = 'Lock' + LCK_M_RX_U = 'Lock' + LCK_M_RX_U_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_U_LOW_PRIORITY = 'Lock' + LCK_M_RX_X = 'Lock' + LCK_M_RX_X_ABORT_BLOCKERS = 'Lock' + LCK_M_RX_X_LOW_PRIORITY = 'Lock' + LCK_M_S = 'Lock' + LCK_M_S_ABORT_BLOCKERS = 'Lock' + LCK_M_S_LOW_PRIORITY = 'Lock' + LCK_M_SCH_M = 'Lock' + LCK_M_SCH_M_ABORT_BLOCKERS = 'Lock' + LCK_M_SCH_M_LOW_PRIORITY = 'Lock' + LCK_M_SCH_S = 'Lock' + LCK_M_SCH_S_ABORT_BLOCKERS = 'Lock' + LCK_M_SCH_S_LOW_PRIORITY = 'Lock' + LCK_M_SIU = 'Lock' + LCK_M_SIU_ABORT_BLOCKERS = 'Lock' + LCK_M_SIU_LOW_PRIORITY = 'Lock' + LCK_M_SIX = 'Lock' + LCK_M_SIX_ABORT_BLOCKERS = 'Lock' + LCK_M_SIX_LOW_PRIORITY = 'Lock' + LCK_M_U = 'Lock' + LCK_M_U_ABORT_BLOCKERS = 'Lock' + LCK_M_U_LOW_PRIORITY = 'Lock' + LCK_M_UIX = 'Lock' + LCK_M_UIX_ABORT_BLOCKERS = 'Lock' + LCK_M_UIX_LOW_PRIORITY = 'Lock' + LCK_M_X = 'Lock' + LCK_M_X_ABORT_BLOCKERS = 'Lock' + LCK_M_X_LOW_PRIORITY = 'Lock' + LOGBUFFER = 'Tran Log IO' + LOGMGR = 'Tran Log IO' + LOGMGR_FLUSH = 'Tran Log IO' + LOGMGR_PMM_LOG = 'Tran Log IO' + LOGMGR_QUEUE = 'Idle' + LOGMGR_RESERVE_APPEND = 'Tran Log IO' + MEMORY_ALLOCATION_EXT = 'Memory' + MEMORY_GRANT_UPDATE = 'Memory' + MSQL_XACT_MGR_MUTEX = 'Transaction' + MSQL_XACT_MUTEX = 'Transaction' + MSSEARCH = 'Full Text Search' + NET_WAITFOR_PACKET = 'Network IO' + ONDEMAND_TASK_QUEUE = 'Idle' + PAGEIOLATCH_DT = 'Buffer IO' + PAGEIOLATCH_EX = 'Buffer IO' + PAGEIOLATCH_KP = 'Buffer IO' + PAGEIOLATCH_NL = 'Buffer IO' + PAGEIOLATCH_SH = 'Buffer IO' + PAGEIOLATCH_UP = 'Buffer IO' + PAGELATCH_DT = 'Buffer Latch' + PAGELATCH_EX = 'Buffer Latch' + PAGELATCH_KP = 'Buffer Latch' + PAGELATCH_NL = 'Buffer Latch' + PAGELATCH_SH = 'Buffer Latch' + PAGELATCH_UP = 'Buffer Latch' + POOL_LOG_RATE_GOVERNOR = 'Log Rate Governor' + PREEMPTIVE_ABR = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPMEDIA = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPTAPE = 'Preemptive' + PREEMPTIVE_CLOSEBACKUPVDIDEVICE = 'Preemptive' + PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL = 'Preemptive' + PREEMPTIVE_COM_COCREATEINSTANCE = 'Preemptive' + PREEMPTIVE_COM_COGETCLASSOBJECT = 'Preemptive' + PREEMPTIVE_COM_CREATEACCESSOR = 'Preemptive' + PREEMPTIVE_COM_DELETEROWS = 'Preemptive' + PREEMPTIVE_COM_GETCOMMANDTEXT = 'Preemptive' + PREEMPTIVE_COM_GETDATA = 'Preemptive' + PREEMPTIVE_COM_GETNEXTROWS = 'Preemptive' + PREEMPTIVE_COM_GETRESULT = 'Preemptive' + PREEMPTIVE_COM_GETROWSBYBOOKMARK = 'Preemptive' + PREEMPTIVE_COM_LBFLUSH = 'Preemptive' + PREEMPTIVE_COM_LBLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_LBREADAT = 'Preemptive' + PREEMPTIVE_COM_LBSETSIZE = 'Preemptive' + PREEMPTIVE_COM_LBSTAT = 'Preemptive' + PREEMPTIVE_COM_LBUNLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_LBWRITEAT = 'Preemptive' + PREEMPTIVE_COM_QUERYINTERFACE = 'Preemptive' + PREEMPTIVE_COM_RELEASE = 'Preemptive' + PREEMPTIVE_COM_RELEASEACCESSOR = 'Preemptive' + PREEMPTIVE_COM_RELEASEROWS = 'Preemptive' + PREEMPTIVE_COM_RELEASESESSION = 'Preemptive' + PREEMPTIVE_COM_RESTARTPOSITION = 'Preemptive' + PREEMPTIVE_COM_SEQSTRMREAD = 'Preemptive' + PREEMPTIVE_COM_SEQSTRMREADANDWRITE = 'Preemptive' + PREEMPTIVE_COM_SETDATAFAILURE = 'Preemptive' + PREEMPTIVE_COM_SETPARAMETERINFO = 'Preemptive' + PREEMPTIVE_COM_SETPARAMETERPROPERTIES = 'Preemptive' + PREEMPTIVE_COM_STRMLOCKREGION = 'Preemptive' + PREEMPTIVE_COM_STRMSEEKANDREAD = 'Preemptive' + PREEMPTIVE_COM_STRMSEEKANDWRITE = 'Preemptive' + PREEMPTIVE_COM_STRMSETSIZE = 'Preemptive' + PREEMPTIVE_COM_STRMSTAT = 'Preemptive' + PREEMPTIVE_COM_STRMUNLOCKREGION = 'Preemptive' + PREEMPTIVE_CONSOLEWRITE = 'Preemptive' + PREEMPTIVE_CREATEPARAM = 'Preemptive' + PREEMPTIVE_DEBUG = 'Preemptive' + PREEMPTIVE_DFSADDLINK = 'Preemptive' + PREEMPTIVE_DFSLINKEXISTCHECK = 'Preemptive' + PREEMPTIVE_DFSLINKHEALTHCHECK = 'Preemptive' + PREEMPTIVE_DFSREMOVELINK = 'Preemptive' + PREEMPTIVE_DFSREMOVEROOT = 'Preemptive' + PREEMPTIVE_DFSROOTFOLDERCHECK = 'Preemptive' + PREEMPTIVE_DFSROOTINIT = 'Preemptive' + PREEMPTIVE_DFSROOTSHARECHECK = 'Preemptive' + PREEMPTIVE_DTC_ABORT = 'Preemptive' + PREEMPTIVE_DTC_ABORTREQUESTDONE = 'Preemptive' + PREEMPTIVE_DTC_BEGINTRANSACTION = 'Preemptive' + PREEMPTIVE_DTC_COMMITREQUESTDONE = 'Preemptive' + PREEMPTIVE_DTC_ENLIST = 'Preemptive' + PREEMPTIVE_DTC_PREPAREREQUESTDONE = 'Preemptive' + PREEMPTIVE_FILESIZEGET = 'Preemptive' + PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION = 'Preemptive' + PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION = 'Preemptive' + PREEMPTIVE_FSAOLEDB_STARTTRANSACTION = 'Preemptive' + PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO = 'Preemptive' + PREEMPTIVE_GETRMINFO = 'Preemptive' + PREEMPTIVE_HADR_LEASE_MECHANISM = 'Preemptive' + PREEMPTIVE_HTTP_EVENT_WAIT = 'Preemptive' + PREEMPTIVE_HTTP_REQUEST = 'Preemptive' + PREEMPTIVE_LOCKMONITOR = 'Preemptive' + PREEMPTIVE_MSS_RELEASE = 'Preemptive' + PREEMPTIVE_ODBCOPS = 'Preemptive' + PREEMPTIVE_OLE_UNINIT = 'Preemptive' + PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN = 'Preemptive' + PREEMPTIVE_OLEDB_ABORTTRAN = 'Preemptive' + PREEMPTIVE_OLEDB_GETDATASOURCE = 'Preemptive' + PREEMPTIVE_OLEDB_GETLITERALINFO = 'Preemptive' + PREEMPTIVE_OLEDB_GETPROPERTIES = 'Preemptive' + PREEMPTIVE_OLEDB_GETPROPERTYINFO = 'Preemptive' + PREEMPTIVE_OLEDB_GETSCHEMALOCK = 'Preemptive' + PREEMPTIVE_OLEDB_JOINTRANSACTION = 'Preemptive' + PREEMPTIVE_OLEDB_RELEASE = 'Preemptive' + PREEMPTIVE_OLEDB_SETPROPERTIES = 'Preemptive' + PREEMPTIVE_OLEDBOPS = 'Preemptive' + PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE = 'Preemptive' + PREEMPTIVE_OS_AUTHENTICATIONOPS = 'Preemptive' + PREEMPTIVE_OS_AUTHORIZATIONOPS = 'Preemptive' + PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT = 'Preemptive' + PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID = 'Preemptive' + PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER = 'Preemptive' + PREEMPTIVE_OS_BACKUPREAD = 'Preemptive' + PREEMPTIVE_OS_CLOSEHANDLE = 'Preemptive' + PREEMPTIVE_OS_CLUSTEROPS = 'Preemptive' + PREEMPTIVE_OS_COMOPS = 'Preemptive' + PREEMPTIVE_OS_COMPLETEAUTHTOKEN = 'Preemptive' + PREEMPTIVE_OS_COPYFILE = 'Preemptive' + PREEMPTIVE_OS_CREATEDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_CREATEFILE = 'Preemptive' + PREEMPTIVE_OS_CRYPTACQUIRECONTEXT = 'Preemptive' + PREEMPTIVE_OS_CRYPTIMPORTKEY = 'Preemptive' + PREEMPTIVE_OS_CRYPTOPS = 'Preemptive' + PREEMPTIVE_OS_DECRYPTMESSAGE = 'Preemptive' + PREEMPTIVE_OS_DELETEFILE = 'Preemptive' + PREEMPTIVE_OS_DELETESECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_DEVICEIOCONTROL = 'Preemptive' + PREEMPTIVE_OS_DEVICEOPS = 'Preemptive' + PREEMPTIVE_OS_DIRSVC_NETWORKOPS = 'Preemptive' + PREEMPTIVE_OS_DISCONNECTNAMEDPIPE = 'Preemptive' + PREEMPTIVE_OS_DOMAINSERVICESOPS = 'Preemptive' + PREEMPTIVE_OS_DSGETDCNAME = 'Preemptive' + PREEMPTIVE_OS_DTCOPS = 'Preemptive' + PREEMPTIVE_OS_ENCRYPTMESSAGE = 'Preemptive' + PREEMPTIVE_OS_FILEOPS = 'Preemptive' + PREEMPTIVE_OS_FINDFILE = 'Preemptive' + PREEMPTIVE_OS_FLUSHFILEBUFFERS = 'Preemptive' + PREEMPTIVE_OS_FORMATMESSAGE = 'Preemptive' + PREEMPTIVE_OS_FREECREDENTIALSHANDLE = 'Preemptive' + PREEMPTIVE_OS_FREELIBRARY = 'Preemptive' + PREEMPTIVE_OS_GENERICOPS = 'Preemptive' + PREEMPTIVE_OS_GETADDRINFO = 'Preemptive' + PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE = 'Preemptive' + PREEMPTIVE_OS_GETDISKFREESPACE = 'Preemptive' + PREEMPTIVE_OS_GETFILEATTRIBUTES = 'Preemptive' + PREEMPTIVE_OS_GETFILESIZE = 'Preemptive' + PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE = 'Preemptive' + PREEMPTIVE_OS_GETLONGPATHNAME = 'Preemptive' + PREEMPTIVE_OS_GETPROCADDRESS = 'Preemptive' + PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT = 'Preemptive' + PREEMPTIVE_OS_GETVOLUMEPATHNAME = 'Preemptive' + PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT = 'Preemptive' + PREEMPTIVE_OS_LIBRARYOPS = 'Preemptive' + PREEMPTIVE_OS_LOADLIBRARY = 'Preemptive' + PREEMPTIVE_OS_LOGONUSER = 'Preemptive' + PREEMPTIVE_OS_LOOKUPACCOUNTSID = 'Preemptive' + PREEMPTIVE_OS_MESSAGEQUEUEOPS = 'Preemptive' + PREEMPTIVE_OS_MOVEFILE = 'Preemptive' + PREEMPTIVE_OS_NETGROUPGETUSERS = 'Preemptive' + PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS = 'Preemptive' + PREEMPTIVE_OS_NETUSERGETGROUPS = 'Preemptive' + PREEMPTIVE_OS_NETUSERGETLOCALGROUPS = 'Preemptive' + PREEMPTIVE_OS_NETUSERMODALSGET = 'Preemptive' + PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY = 'Preemptive' + PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE = 'Preemptive' + PREEMPTIVE_OS_OPENDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_PDH_WMI_INIT = 'Preemptive' + PREEMPTIVE_OS_PIPEOPS = 'Preemptive' + PREEMPTIVE_OS_PROCESSOPS = 'Preemptive' + PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES = 'Preemptive' + PREEMPTIVE_OS_QUERYREGISTRY = 'Preemptive' + PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN = 'Preemptive' + PREEMPTIVE_OS_REMOVEDIRECTORY = 'Preemptive' + PREEMPTIVE_OS_REPORTEVENT = 'Preemptive' + PREEMPTIVE_OS_REVERTTOSELF = 'Preemptive' + PREEMPTIVE_OS_RSFXDEVICEOPS = 'Preemptive' + PREEMPTIVE_OS_SECURITYOPS = 'Preemptive' + PREEMPTIVE_OS_SERVICEOPS = 'Preemptive' + PREEMPTIVE_OS_SETENDOFFILE = 'Preemptive' + PREEMPTIVE_OS_SETFILEPOINTER = 'Preemptive' + PREEMPTIVE_OS_SETFILEVALIDDATA = 'Preemptive' + PREEMPTIVE_OS_SETNAMEDSECURITYINFO = 'Preemptive' + PREEMPTIVE_OS_SQLCLROPS = 'Preemptive' + PREEMPTIVE_OS_SQMLAUNCH = 'Preemptive' + PREEMPTIVE_OS_VERIFYSIGNATURE = 'Preemptive' + PREEMPTIVE_OS_VERIFYTRUST = 'Preemptive' + PREEMPTIVE_OS_VSSOPS = 'Preemptive' + PREEMPTIVE_OS_WAITFORSINGLEOBJECT = 'Preemptive' + PREEMPTIVE_OS_WINSOCKOPS = 'Preemptive' + PREEMPTIVE_OS_WRITEFILE = 'Preemptive' + PREEMPTIVE_OS_WRITEFILEGATHER = 'Preemptive' + PREEMPTIVE_OS_WSASETLASTERROR = 'Preemptive' + PREEMPTIVE_REENLIST = 'Preemptive' + PREEMPTIVE_RESIZELOG = 'Preemptive' + PREEMPTIVE_ROLLFORWARDREDO = 'Preemptive' + PREEMPTIVE_ROLLFORWARDUNDO = 'Preemptive' + PREEMPTIVE_SB_STOPENDPOINT = 'Preemptive' + PREEMPTIVE_SERVER_STARTUP = 'Preemptive' + PREEMPTIVE_SETRMINFO = 'Preemptive' + PREEMPTIVE_SHAREDMEM_GETDATA = 'Preemptive' + PREEMPTIVE_SNIOPEN = 'Preemptive' + PREEMPTIVE_SOSHOST = 'Preemptive' + PREEMPTIVE_SOSTESTING = 'Preemptive' + PREEMPTIVE_SP_SERVER_DIAGNOSTICS = 'Preemptive' + PREEMPTIVE_STARTRM = 'Preemptive' + PREEMPTIVE_STREAMFCB_CHECKPOINT = 'Preemptive' + PREEMPTIVE_STREAMFCB_RECOVER = 'Preemptive' + PREEMPTIVE_STRESSDRIVER = 'Preemptive' + PREEMPTIVE_TESTING = 'Preemptive' + PREEMPTIVE_TRANSIMPORT = 'Preemptive' + PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN = 'Preemptive' + PREEMPTIVE_VSS_CREATESNAPSHOT = 'Preemptive' + PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT = 'Preemptive' + PREEMPTIVE_XE_CALLBACKEXECUTE = 'Preemptive' + PREEMPTIVE_XE_CX_FILE_OPEN = 'Preemptive' + PREEMPTIVE_XE_CX_HTTP_CALL = 'Preemptive' + PREEMPTIVE_XE_DISPATCHER = 'Preemptive' + PREEMPTIVE_XE_ENGINEINIT = 'Preemptive' + PREEMPTIVE_XE_GETTARGETSTATE = 'Preemptive' + PREEMPTIVE_XE_SESSIONCOMMIT = 'Preemptive' + PREEMPTIVE_XE_TARGETFINALIZE = 'Preemptive' + PREEMPTIVE_XE_TARGETINIT = 'Preemptive' + PREEMPTIVE_XE_TIMERRUN = 'Preemptive' + PREEMPTIVE_XETESTING = 'Preemptive' + PWAIT_HADR_ACTION_COMPLETED = 'Replication' + PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC = 'Replication' + PWAIT_HADR_CLUSTER_INTEGRATION = 'Replication' + PWAIT_HADR_FAILOVER_COMPLETED = 'Replication' + PWAIT_HADR_JOIN = 'Replication' + PWAIT_HADR_OFFLINE_COMPLETED = 'Replication' + PWAIT_HADR_ONLINE_COMPLETED = 'Replication' + PWAIT_HADR_POST_ONLINE_COMPLETED = 'Replication' + PWAIT_HADR_SERVER_READY_CONNECTIONS = 'Replication' + PWAIT_HADR_WORKITEM_COMPLETED = 'Replication' + PWAIT_HADRSIM = 'Replication' + PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC = 'Full Text Search' + QUERY_TRACEOUT = 'Tracing' + REPL_CACHE_ACCESS = 'Replication' + REPL_HISTORYCACHE_ACCESS = 'Replication' + REPL_SCHEMA_ACCESS = 'Replication' + REPL_TRANFSINFO_ACCESS = 'Replication' + REPL_TRANHASHTABLE_ACCESS = 'Replication' + REPL_TRANTEXTINFO_ACCESS = 'Replication' + REPLICA_WRITES = 'Replication' + REQUEST_FOR_DEADLOCK_SEARCH = 'Idle' + RESERVED_MEMORY_ALLOCATION_EXT = 'Memory' + RESOURCE_SEMAPHORE = 'Memory' + RESOURCE_SEMAPHORE_QUERY_COMPILE = 'Compilation' + SLEEP_BPOOL_FLUSH = 'Idle' + SLEEP_BUFFERPOOL_HELPLW = 'Idle' + SLEEP_DBSTARTUP = 'Idle' + SLEEP_DCOMSTARTUP = 'Idle' + SLEEP_MASTERDBREADY = 'Idle' + SLEEP_MASTERMDREADY = 'Idle' + SLEEP_MASTERUPGRADED = 'Idle' + SLEEP_MEMORYPOOL_ALLOCATEPAGES = 'Idle' + SLEEP_MSDBSTARTUP = 'Idle' + SLEEP_RETRY_VIRTUALALLOC = 'Idle' + SLEEP_SYSTEMTASK = 'Idle' + SLEEP_TASK = 'Idle' + SLEEP_TEMPDBSTARTUP = 'Idle' + SLEEP_WORKSPACE_ALLOCATEPAGE = 'Idle' + SOS_SCHEDULER_YIELD = 'CPU' + SQLCLR_APPDOMAIN = 'SQL CLR' + SQLCLR_ASSEMBLY = 'SQL CLR' + SQLCLR_DEADLOCK_DETECTION = 'SQL CLR' + SQLCLR_QUANTUM_PUNISHMENT = 'SQL CLR' + SQLTRACE_BUFFER_FLUSH = 'Idle' + SQLTRACE_FILE_BUFFER = 'Tracing' + SQLTRACE_FILE_READ_IO_COMPLETION = 'Tracing' + SQLTRACE_FILE_WRITE_IO_COMPLETION = 'Tracing' + SQLTRACE_INCREMENTAL_FLUSH_SLEEP = 'Idle' + SQLTRACE_PENDING_BUFFER_WRITERS = 'Tracing' + SQLTRACE_SHUTDOWN = 'Tracing' + SQLTRACE_WAIT_ENTRIES = 'Idle' + THREADPOOL = 'Worker Thread' + TRACE_EVTNOTIF = 'Tracing' + TRACEWRITE = 'Tracing' + TRAN_MARKLATCH_DT = 'Transaction' + TRAN_MARKLATCH_EX = 'Transaction' + TRAN_MARKLATCH_KP = 'Transaction' + TRAN_MARKLATCH_NL = 'Transaction' + TRAN_MARKLATCH_SH = 'Transaction' + TRAN_MARKLATCH_UP = 'Transaction' + TRANSACTION_MUTEX = 'Transaction' + WAIT_FOR_RESULTS = 'User Wait' + WAITFOR = 'User Wait' + WRITE_COMPLETION = 'Other Disk IO' + WRITELOG = 'Tran Log IO' + XACT_OWN_TRANSACTION = 'Transaction' + XACT_RECLAIM_SESSION = 'Transaction' + XACTLOCKINFO = 'Transaction' + XACTWORKSPACE_MUTEX = 'Transaction' + XE_DISPATCHER_WAIT = 'Idle' + XE_TIMER_EVENT = 'Idle' + ABR = 'Other' + ASSEMBLY_LOAD = 'SQLCLR' + ASYNC_DISKPOOL_LOCK = 'Buffer I/O' + BACKUP = 'Backup' + BACKUP_CLIENTLOCK = 'Backup' + BACKUP_OPERATOR = 'Backup' + BACKUPBUFFER = 'Backup' + BACKUPTHREAD = 'Backup' + BAD_PAGE_PROCESS = 'Other' + BUILTIN_HASHKEY_MUTEX = 'Other' + CHECK_PRINT_RECORD = 'Other' + CPU = 'CPU' + CURSOR = 'Other' + CURSOR_ASYNC = 'Other' + DAC_INIT = 'Other' + DBCC_COLUMN_TRANSLATION_CACHE = 'Other' + DBTABLE = 'Other' + DEADLOCK_ENUM_MUTEX = 'Latch' + DEADLOCK_TASK_SEARCH = 'Other' + DEBUG = 'Other' + DISABLE_VERSIONING = 'Other' + DISKIO_SUSPEND = 'Backup' + DLL_LOADING_MUTEX = 'Other' + DROPTEMP = 'Other' + DUMP_LOG_COORDINATOR = 'Other' + DUMP_LOG_COORDINATOR_QUEUE = 'Other' + DUMPTRIGGER = 'Other' + EC = 'Other' + EE_SPECPROC_MAP_INIT = 'Other' + ENABLE_VERSIONING = 'Other' + ERROR_REPORTING_MANAGER = 'Other' + EXECSYNC = 'Parallelism' + EXECUTION_PIPE_EVENT_INTERNAL = 'Other' + FAILPOINT = 'Other' + FS_GARBAGE_COLLECTOR_SHUTDOWN = 'SQLCLR' + FSAGENT = 'Idle' + FT_RESUME_CRAWL = 'Other' + GUARDIAN = 'Other' + HTTP_ENDPOINT_COLLCREATE = 'Other' + HTTP_ENUMERATION = 'Other' + HTTP_START = 'Other' + IMP_IMPORT_MUTEX = 'Other' + IMPPROV_IOWAIT = 'Other' + INDEX_USAGE_STATS_MUTEX = 'Latch' + INTERNAL_TESTING = 'Other' + IO_AUDIT_MUTEX = 'Other' + KSOURCE_WAKEUP = 'Idle' + KTM_ENLISTMENT = 'Other' + KTM_RECOVERY_MANAGER = 'Other' + KTM_RECOVERY_RESOLUTION = 'Other' + LOWFAIL_MEMMGR_QUEUE = 'Memory' + MIRROR_SEND_MESSAGE = 'Other' + MISCELLANEOUS = 'Other' + MSQL_DQ = 'Network I/O' + MSQL_SYNC_PIPE = 'Other' + MSQL_XP = 'Other' + OLEDB = 'Network I/O' + PARALLEL_BACKUP_QUEUE = 'Other' + PRINT_ROLLBACK_PROGRESS = 'Other' + QNMANAGER_ACQUIRE = 'Other' + QPJOB_KILL = 'Other' + QPJOB_WAITFOR_ABORT = 'Other' + QRY_MEM_GRANT_INFO_MUTEX = 'Other' + QUERY_ERRHDL_SERVICE_DONE = 'Other' + QUERY_EXECUTION_INDEX_SORT_EVENT_OPEN = 'Other' + QUERY_NOTIFICATION_MGR_MUTEX = 'Other' + QUERY_NOTIFICATION_SUBSCRIPTION_MUTEX = 'Other' + QUERY_NOTIFICATION_TABLE_MGR_MUTEX = 'Other' + QUERY_NOTIFICATION_UNITTEST_MUTEX = 'Other' + QUERY_OPTIMIZER_PRINT_MUTEX = 'Other' + QUERY_REMOTE_BRICKS_DONE = 'Other' + RECOVER_CHANGEDB = 'Other' + REQUEST_DISPENSER_PAUSE = 'Other' + RESOURCE_QUEUE = 'Idle' + RESOURCE_SEMAPHORE_MUTEX = 'Compilation' + RESOURCE_SEMAPHORE_SMALL_QUERY = 'Compilation' + SEC_DROP_TEMP_KEY = 'Other' + SEQUENTIAL_GUID = 'Other' + SERVER_IDLE_CHECK = 'Idle' + SHUTDOWN = 'Other' + SNI_CRITICAL_SECTION = 'Other' + SNI_HTTP_ACCEPT = 'Idle' + SNI_HTTP_WAITFOR_0_DISCON = 'Other' + SNI_LISTENER_ACCESS = 'Other' + SNI_TASK_COMPLETION = 'Other' + SOAP_READ = 'Full Text Search' + SOAP_WRITE = 'Full Text Search' + SOS_CALLBACK_REMOVAL = 'Other' + SOS_DISPATCHER_MUTEX = 'Other' + SOS_LOCALALLOCATORLIST = 'Other' + SOS_OBJECT_STORE_DESTROY_MUTEX = 'Other' + SOS_PROCESS_AFFINITY_MUTEX = 'Other' + SOS_RESERVEDMEMBLOCKLIST = 'Memory' + SOS_STACKSTORE_INIT_MUTEX = 'Other' + SOS_SYNC_TASK_ENQUEUE_EVENT = 'Other' + SOS_VIRTUALMEMORY_LOW = 'Memory' + SOSHOST_EVENT = 'Other' + SOSHOST_INTERNAL = 'Other' + SOSHOST_MUTEX = 'Other' + SOSHOST_RWLOCK = 'Other' + SOSHOST_SEMAPHORE = 'Other' + SOSHOST_SLEEP = 'Other' + SOSHOST_TRACELOCK = 'Other' + SOSHOST_WAITFORDONE = 'Other' + SQLSORT_NORMMUTEX = 'Other' + SQLSORT_SORTMUTEX = 'Other' + SQLTRACE_LOCK = 'Other' + SRVPROC_SHUTDOWN = 'Other' + TEMPOBJ = 'Other' + TIMEPRIV_TIMEPERIOD = 'Other' + UTIL_PAGE_ALLOC = 'Memory' + VIA_ACCEPT = 'Other' + VIEW_DEFINITION_MUTEX = 'Latch' + WAITFOR_TASKSHUTDOWN = 'Idle' + WAITSTAT_MUTEX = 'Other' + WCC = 'Other' + WORKTBL_DROP = 'Other' + XE_BUFFERMGR_ALLPROCECESSED_EVENT = 'Other' + XE_BUFFERMGR_FREEBUF_EVENT = 'Other' + XE_DISPATCHER_JOIN = 'Other' + XE_MODULEMGR_SYNC = 'Other' + XE_OLS_LOCK = 'Other' + XE_SERVICES_MUTEX = 'Other' + XE_SESSION_CREATE_SYNC = 'Other' + XE_SESSION_SYNC = 'Other' + XE_STM_CREATE = 'Other' + XE_TIMER_MUTEX = 'Other' + XE_TIMER_TASK_DONE = 'Other' + } + + $ignorable = 'BROKER_EVENTHANDLER', 'BROKER_RECEIVE_WAITFOR', 'BROKER_TASK_STOP', + 'BROKER_TO_FLUSH', 'BROKER_TRANSMITTER', 'CHECKPOINT_QUEUE', + 'CHKPT', 'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'CLR_SEMAPHORE', + 'DBMIRROR_DBM_EVENT', 'DBMIRROR_EVENTS_QUEUE', 'DBMIRROR_WORKER_QUEUE', + 'DBMIRRORING_CMD', 'DIRTY_PAGE_POLL', 'DISPATCHER_QUEUE_SEMAPHORE', + 'EXECSYNC', 'FSAGENT', 'FT_IFTS_SCHEDULER_IDLE_WAIT', 'FT_IFTSHC_MUTEX', + 'HADR_CLUSAPI_CALL', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'HADR_LOGCAPTURE_WAIT', + 'HADR_NOTIFICATION_DEQUEUE', 'HADR_TIMER_TASK', 'HADR_WORK_QUEUE', + 'KSOURCE_WAKEUP', 'LAZYWRITER_SLEEP', 'LOGMGR_QUEUE', + 'MEMORY_ALLOCATION_EXT', 'ONDEMAND_TASK_QUEUE', 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS', + 'PARALLEL_REDO_WORKER_WAIT_WORK', 'PREEMPTIVE_HADR_LEASE_MECHANISM', + 'PREEMPTIVE_OS_LIBRARYOPS', 'PREEMPTIVE_OS_COMOPS', 'PREEMPTIVE_OS_CRYPTOPS', + 'PREEMPTIVE_OS_PIPEOPS', 'PREEMPTIVE_OS_AUTHENTICATIONOPS', + 'PREEMPTIVE_OS_GENERICOPS', 'PREEMPTIVE_OS_VERIFYTRUST', + 'PREEMPTIVE_OS_FILEOPS', 'PREEMPTIVE_OS_DEVICEOPS', 'PREEMPTIVE_OS_QUERYREGISTRY', + 'PREEMPTIVE_OS_WRITEFILE', 'PREEMPTIVE_XE_CALLBACKEXECUTE', 'PREEMPTIVE_XE_DISPATCHER', + 'PREEMPTIVE_XE_GETTARGETSTATE', 'PREEMPTIVE_XE_SESSIONCOMMIT', + 'PREEMPTIVE_XE_TARGETINIT', 'PREEMPTIVE_XE_TARGETFINALIZE', + 'PWAIT_ALL_COMPONENTS_INITIALIZED', 'PWAIT_DIRECTLOGCONSUMER_GETNEXT', + 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'QDS_ASYNC_QUEUE', + 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', + 'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_BPOOL_FLUSH', 'SLEEP_DBSTARTUP', + 'SLEEP_DCOMSTARTUP', 'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', + 'SLEEP_MASTERUPGRADED', 'SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK', 'SLEEP_TASK', + 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT', 'SP_SERVER_DIAGNOSTICS_SLEEP', + 'SQLTRACE_BUFFER_FLUSH', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'SQLTRACE_WAIT_ENTRIES', + 'WAIT_FOR_RESULTS', 'WAITFOR', 'WAITFOR_TASKSHUTDOWN', 'WAIT_XTP_HOST_WAIT', + 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'WAIT_XTP_CKPT_CLOSE', 'WAIT_XTP_RECOVERY', + 'XE_BUFFERMGR_ALLPROCESSED_EVENT', 'XE_DISPATCHER_JOIN', + 'XE_DISPATCHER_WAIT', 'XE_LIVE_TARGET_TVF', 'XE_TIMER_EVENT' + + if ($IncludeIgnorable) { + $sql = "WITH [Waits] AS + (SELECT + [wait_type], + [wait_time_ms] / 1000.0 AS [WaitS], + ([wait_time_ms] - [signal_wait_time_ms]) / 1000.0 AS [ResourceS], + [signal_wait_time_ms] / 1000.0 AS [SignalS], + [waiting_tasks_count] AS [WaitCount], + 100.0 * [wait_time_ms] / SUM ([wait_time_ms]) OVER() AS [Percentage], + ROW_NUMBER() OVER(ORDER BY [wait_time_ms] DESC) AS [RowNum] + FROM sys.dm_os_wait_stats + WHERE [waiting_tasks_count] > 0 + ) + SELECT + MAX ([W1].[wait_type]) AS [WaitType], + CAST (MAX ([W1].[WaitS]) AS DECIMAL (16,2)) AS [WaitSeconds], + CAST (MAX ([W1].[ResourceS]) AS DECIMAL (16,2)) AS [ResourceSeconds], + CAST (MAX ([W1].[SignalS]) AS DECIMAL (16,2)) AS [SignalSeconds], + MAX ([W1].[WaitCount]) AS [WaitCount], + CAST (MAX ([W1].[Percentage]) AS DECIMAL (5,2)) AS [Percentage], + CAST ((MAX ([W1].[WaitS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgWaitSeconds], + CAST ((MAX ([W1].[ResourceS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgResSeconds], + CAST ((MAX ([W1].[SignalS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgSigSeconds], + CAST ('https://www.sqlskills.com/help/waits/' + MAX ([W1].[wait_type]) as XML) AS [URL] + FROM [Waits] AS [W1] + INNER JOIN [Waits] AS [W2] + ON [W2].[RowNum] <= [W1].[RowNum] + GROUP BY [W1].[RowNum] HAVING SUM ([W2].[Percentage]) - MAX([W1].[Percentage]) < $Threshold" + } + else { + $IgnorableList = "'$($ignorable -join "','")'" + $sql = "WITH [Waits] AS + (SELECT + [wait_type], + [wait_time_ms] / 1000.0 AS [WaitS], + ([wait_time_ms] - [signal_wait_time_ms]) / 1000.0 AS [ResourceS], + [signal_wait_time_ms] / 1000.0 AS [SignalS], + [waiting_tasks_count] AS [WaitCount], + 100.0 * [wait_time_ms] / SUM ([wait_time_ms]) OVER() AS [Percentage], + ROW_NUMBER() OVER(ORDER BY [wait_time_ms] DESC) AS [RowNum] + FROM sys.dm_os_wait_stats + WHERE [waiting_tasks_count] > 0 + AND Cast([wait_type] as VARCHAR(60)) NOT IN ($IgnorableList) + ) + SELECT + MAX ([W1].[wait_type]) AS [WaitType], + CAST (MAX ([W1].[WaitS]) AS DECIMAL (16,2)) AS [WaitSeconds], + CAST (MAX ([W1].[ResourceS]) AS DECIMAL (16,2)) AS [ResourceSeconds], + CAST (MAX ([W1].[SignalS]) AS DECIMAL (16,2)) AS [SignalSeconds], + MAX ([W1].[WaitCount]) AS [WaitCount], + CAST (MAX ([W1].[Percentage]) AS DECIMAL (5,2)) AS [Percentage], + CAST ((MAX ([W1].[WaitS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgWaitSeconds], + CAST ((MAX ([W1].[ResourceS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgResSeconds], + CAST ((MAX ([W1].[SignalS]) / MAX ([W1].[WaitCount])) AS DECIMAL (16,4)) AS [AvgSigSeconds], + CAST ('https://www.sqlskills.com/help/waits/' + MAX ([W1].[wait_type]) as XML) AS [URL] + FROM [Waits] AS [W1] + INNER JOIN [Waits] AS [W2] + ON [W2].[RowNum] <= [W1].[RowNum] + GROUP BY [W1].[RowNum] HAVING SUM ([W2].[Percentage]) - MAX([W1].[Percentage]) < $Threshold" + + } + Write-Message -Level Debug -Message $sql + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + Write-Message -Level Verbose -Message "Connected to $instance" + if ($IncludeIgnorable) { + $excludeColumns = 'Notes' + } + else { + $excludeColumns = 'Notes', 'Ignorable' + } + + foreach ($row in $server.Query($sql)) { + $waitType = $row.WaitType + if (-not $IncludeIgnorable) { + if ($ignorable -contains $waitType) { continue } + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + WaitType = $waitType + Category = ($category).$waitType + WaitSeconds = $row.WaitSeconds + ResourceSeconds = $row.ResourceSeconds + SignalSeconds = $row.SignalSeconds + WaitCount = $row.WaitCount + Percentage = $row.Percentage + AverageWaitSeconds = $row.AvgWaitSeconds + AverageResourceSeconds = $row.AvgResSeconds + AverageSignalSeconds = $row.AvgSigSeconds + Ignorable = ($ignorable -contains $waitType) + URL = $row.URL + Notes = ($details).$waitType + } | Select-DefaultView -ExcludeProperty $excludeColumns + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaWaitingTask.ps1 b/functions/Get-DbaWaitingTask.ps1 new file mode 100644 index 000000000000..e93a29f3563b --- /dev/null +++ b/functions/Get-DbaWaitingTask.ps1 @@ -0,0 +1,137 @@ +function Get-DbaWaitingTask { + <# + .SYNOPSIS + Displays waiting task. + + .DESCRIPTION + This command is based on waiting task T-SQL script published by Paul Randal. + Reference: https://www.sqlskills.com/blogs/paul/updated-sys-dm_os_waiting_tasks-script-2/ + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version XXXX or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Spid + Find the waiting task of one or more specific process ids + + .PARAMETER IncludeSystemSpid + If this switch is enabled, the output will include the system sessions. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Waits,Task,WaitTask + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWaitingTask + + .EXAMPLE + Get-DbaWaitingTask -SqlInstance sqlserver2014a + + Returns the waiting task for all sessions on sqlserver2014a + + .EXAMPLE + Get-DbaWaitingTask -SqlInstance sqlserver2014a -IncludeSystemSpid + + Returns the waiting task for all sessions (user and system) on sqlserver2014a + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(ValueFromPipelineByPropertyName = $true)] + [object[]]$Spid, + [switch]$IncludeSystemSpid, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $sql = " + SELECT + [owt].[session_id] AS [Spid], + [owt].[exec_context_id] AS [Thread], + [ot].[scheduler_id] AS [Scheduler], + [owt].[wait_duration_ms] AS [WaitMs], + [owt].[wait_type] AS [WaitType], + [owt].[blocking_session_id] AS [BlockingSpid], + [owt].[resource_description] AS [ResourceDesc], + CASE [owt].[wait_type] + WHEN N'CXPACKET' THEN + RIGHT ([owt].[resource_description], + CHARINDEX (N'=', REVERSE ([owt].[resource_description])) - 1) + ELSE NULL + END AS [NodeId], + [eqmg].[dop] AS [Dop], + [er].[database_id] AS [DbId], + [est].text AS [SqlText], + [eqp].[query_plan] AS [QueryPlan], + CAST ('https://www.sqlskills.com/help/waits/' + [owt].[wait_type] as XML) AS [URL] + FROM sys.dm_os_waiting_tasks [owt] + INNER JOIN sys.dm_os_tasks [ot] ON + [owt].[waiting_task_address] = [ot].[task_address] + INNER JOIN sys.dm_exec_sessions [es] ON + [owt].[session_id] = [es].[session_id] + INNER JOIN sys.dm_exec_requests [er] ON + [es].[session_id] = [er].[session_id] + FULL JOIN sys.dm_exec_query_memory_grants [eqmg] ON + [owt].[session_id] = [eqmg].[session_id] + OUTER APPLY sys.dm_exec_sql_text ([er].[sql_handle]) [est] + OUTER APPLY sys.dm_exec_query_plan ([er].[plan_handle]) [eqp] + WHERE + [es].[is_user_process] = $(if (Test-Bound 'IncludeSystemSpid') {0} else {1}) + ORDER BY + [owt].[session_id], + [owt].[exec_context_id] + OPTION(RECOMPILE);" + } + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $results = $server.Query($sql) + foreach ($row in $results) { + if (Test-Bound 'Spid') { + if ($row.Spid -notin $Spid) { continue } + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Spid = $row.Spid + Thread = $row.Thread + Scheduler = $row.Scheduler + WaitMs = $row.WaitMs + WaitType = $row.WaitType + BlockingSpid = $row.BlockingSpid + ResourceDesc = $row.ResourceDesc + NodeId = $row.NodeId + Dop = $row.Dop + DbId = $row.DbId + SqlText = $row.SqlText + QueryPlan = $row.QueryPlan + InfoUrl = $row.InfoUrl + } | Select-DefaultView -ExcludeProperty 'SqlText', 'QueryPlan', 'InfoUrl' + } + } + } +} diff --git a/functions/Get-DbaWindowsLog.ps1 b/functions/Get-DbaWindowsLog.ps1 new file mode 100644 index 000000000000..472e092a4ad6 --- /dev/null +++ b/functions/Get-DbaWindowsLog.ps1 @@ -0,0 +1,302 @@ +function Get-DbaWindowsLog { + <# + .SYNOPSIS + Gets Windows Application events associated with an instance + + .DESCRIPTION + Gets Windows Application events associated with an instance + + .PARAMETER SqlInstance + The instance(s) to retrieve the event logs from + + .PARAMETER Start + Default: 1970 + Retrieve all events starting from this timestamp. + + .PARAMETER End + Default: Now + Retrieve all events that happened before this timestamp + + .PARAMETER Credential + Credential to be used to connect to the Server. Note this is a Windows credential, as this command requires we communicate with the computer and not with the SQL instance. + + .PARAMETER MaxThreads + Default: Unlimited + The maximum number of parallel threads used on the local computer. + Given that those will mostly be waiting for the remote system, there is usually no need to limit this. + + .PARAMETER MaxRemoteThreads + Default: 2 + The maximum number of parallel threads that are executed on the target sql server. + These processes will cause considerable CPU load, so a low limit is advisable in most scenarios. + Any value lower than 1 disables the limit + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Logging + Author: Drew Furgiuele + Editor: Friedrich "Fred" Weinmann + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaWindowsLog + + .EXAMPLE + $ErrorLogs = Get-DbaWindowsLog -SqlInstance sql01\sharepoint + $ErrorLogs | Where-Object ErrorNumber -eq 18456 + + Returns all lines in the errorlogs that have event number 18456 in them + +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [DateTime] + $Start = "1/1/1970 00:00:00", + + [DateTime] + $End = (Get-Date), + + + [System.Management.Automation.PSCredential] + $Credential, + + [int] + $MaxThreads = 0, + + [int] + $MaxRemoteThreads = 2, + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + Write-Message -Level Debug -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + #region Helper Functions + function Start-Runspace { + $Powershell = [PowerShell]::Create().AddScript($scriptBlock_ParallelRemoting).AddParameter("SqlInstance", $instance).AddParameter("Start", $Start).AddParameter("End", $End).AddParameter("Credential", $Credential).AddParameter("MaxRemoteThreads", $MaxRemoteThreads).AddParameter("ScriptBlock", $scriptBlock_RemoteExecution) + $Powershell.RunspacePool = $RunspacePool + Write-Message -Level Verbose -Message "Launching remote runspace against $instance" -Target $instance + $null = $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{ Runspace = $PowerShell.BeginInvoke(); PowerShell = $PowerShell; Instance = $instance.FullSmoName })) + } + + function Receive-Runspace { + [Parameter()] + Param ( + [switch] + $Wait + ) + + do { + foreach ($Run in $RunspaceCollection.ToArray()) { + if ($Run.Runspace.IsCompleted) { + Write-Message -Level Verbose -Message "Receiving results from $($Run.Instance)" -Target $Run.Instance + $Run.PowerShell.EndInvoke($Run.Runspace) + $Run.PowerShell.Dispose() + $RunspaceCollection.Remove($Run) + } + } + + if ($Wait -and ($RunspaceCollection.Count -gt 0)) { Start-Sleep -Milliseconds 250 } + } + while ($Wait -and ($RunspaceCollection.Count -gt 0)) + } + #endregion Helper Functions + + #region Scriptblocks + $scriptBlock_RemoteExecution = { + Param ( + [System.DateTime] + $Start, + + [System.DateTime] + $End, + + [string] + $InstanceName, + + [int] + $Throttle + ) + + #region Helper function + function Convert-ErrorRecord { + Param ( + $Line + ) + + if (Get-Variable -Name codesAndStuff -Scope 1) { + $line2 = (Get-Variable -Name codesAndStuff -Scope 1).Value + Remove-Variable -Name codesAndStuff -Scope 1 + + $groups = [regex]::Matches($line2, '^([\d- :]+.\d\d) (\w+)[ ]+Error: (\d+), Severity: (\d+), State: (\d+)').Groups + $groups2 = [regex]::Matches($line, '^[\d- :]+.\d\d \w+[ ]+(.*)$').Groups + + New-Object PSObject -Property @{ + Timestamp = [DateTime]::ParseExact($groups[1].Value, "yyyy-MM-dd HH:mm:ss.ff", $null) + Spid = $groups[2].Value + Message = $groups2[1].Value + ErrorNumber = [int]($groups[3].Value) + Severity = [int]($groups[4].Value) + State = [int]($groups[5].Value) + } + } + + if ($Line -match '^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d[\w ]+((\w+): (\d+)[,\.]\s?){3}') { + Set-Variable -Name codesAndStuff -Value $Line -Scope 1 + } + } + #endregion Helper function + + #region Script that processes an individual file + $scriptBlock = { + Param ( + [System.IO.FileInfo] + $File + ) + + try { + $stream = New-Object System.IO.FileStream($File.FullName, "Open", "Read", "ReadWrite, Delete") + $reader = New-Object System.IO.StreamReader($stream) + + while (-not $reader.EndOfStream) { + Convert-ErrorRecord -Line $reader.ReadLine() + } + } + catch { } + } + #endregion Script that processes an individual file + + #region Gather list of files to process + $eventSource = "MSSQLSERVER" + if ($InstanceName -notmatch "^DEFAULT$|^MSSQLSERVER$") { + $eventSource = 'MSSQL$' + $InstanceName + } + + $event = Get-WinEvent -FilterHashtable @{ + LogName = "Application" + ID = 17111 + ProviderName = $eventSource + } -MaxEvents 1 -ErrorAction SilentlyContinue + + if (-not $event) { return } + + $path = $event.Properties[0].Value + $errorLogPath = Split-Path -Path $path + $errorLogFileName = Split-Path -Path $path -Leaf + $errorLogFiles = Get-ChildItem -Path $errorLogPath | Where-Object { ($_.Name -like "$errorLogFileName*") -and ($_.LastWriteTime -gt $Start) -and ($_.CreationTime -lt $End) } + #endregion Gather list of files to process + + #region Prepare Runspaces + [Collections.Arraylist]$RunspaceCollection = @() + + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $Command = Get-Item function:Convert-ErrorRecord + $InitialSessionState.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($command.Name, $command.Definition))) + + $RunspacePool = [RunspaceFactory]::CreateRunspacePool($InitialSessionState) + $null = $RunspacePool.SetMinRunspaces(1) + if ($Throttle -gt 0) { $null = $RunspacePool.SetMaxRunspaces($Throttle) } + $RunspacePool.Open() + #endregion Prepare Runspaces + + #region Process Error files + $countDone = 0 + $countStarted = 0 + $countTotal = ($errorLogFiles | Measure-Object).Count + + while ($countDone -lt $countTotal) { + while (($RunspacePool.GetAvailableRunspaces() -gt 0) -and ($countStarted -lt $countTotal)) { + $Powershell = [PowerShell]::Create().AddScript($scriptBlock).AddParameter("File", $errorLogFiles[$countStarted]) + $Powershell.RunspacePool = $RunspacePool + $null = $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{ Runspace = $PowerShell.BeginInvoke(); PowerShell = $PowerShell })) + $countStarted++ + } + + foreach ($Run in $RunspaceCollection.ToArray()) { + if ($Run.Runspace.IsCompleted) { + $Run.PowerShell.EndInvoke($Run.Runspace) | Where-Object { ($_.Timestamp -gt $Start) -and ($_.Timestamp -lt $End) } + $Run.PowerShell.Dispose() + $RunspaceCollection.Remove($Run) + $countDone++ + } + } + + Start-Sleep -Milliseconds 250 + } + $RunspacePool.Close() + $RunspacePool.Dispose() + #endregion Process Error files + } + + $scriptBlock_ParallelRemoting = { + Param ( + [DbaInstanceParameter] + $SqlInstance, + + [DateTime] + $Start, + + [DateTime] + $End, + + [object] + $Credential, + + [int] + $MaxRemoteThreads, + + [System.Management.Automation.ScriptBlock] + $ScriptBlock + ) + + $params = @{ + ArgumentList = $Start, $End, $SqlInstance.InstanceName, $MaxRemoteThreads + ScriptBlock = $ScriptBlock + } + if (-not $SqlInstance.IsLocalhost) { $params["ComputerName"] = $SqlInstance.ComputerName } + if ($Credential) { $params["Credential"] = $Credential } + + Invoke-Command @params | Select-Object @{ n = "InstanceName"; e = { $SqlInstance.FullSmoName } }, Timestamp, Spid, Severity, ErrorNumber, State, Message + } + #endregion Scriptblocks + + #region Setup Runspace + [Collections.Arraylist]$RunspaceCollection = @() + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $RunspacePool = [RunspaceFactory]::CreateRunspacePool($InitialSessionState) + $RunspacePool.SetMinRunspaces(1) | Out-Null + if ($MaxThreads -gt 0) { $null = $RunspacePool.SetMaxRunspaces($MaxThreads) } + $RunspacePool.Open() + + $countStarted = 0 + $countReceived = 0 + #endregion Setup Runspace + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + Start-Runspace + Receive-Runspace + } + } + + end { + Receive-Runspace -Wait + $RunspacePool.Close() + $RunspacePool.Dispose() + } +} \ No newline at end of file diff --git a/functions/Get-DbaXEObject.ps1 b/functions/Get-DbaXEObject.ps1 new file mode 100644 index 000000000000..9b8ceb4aedfd --- /dev/null +++ b/functions/Get-DbaXEObject.ps1 @@ -0,0 +1,110 @@ +function Get-DbaXEObject { + <# + .SYNOPSIS + Gets a list of trace(s) from specified SQL Server instance(s). + + .DESCRIPTION + This function returns a list of Traces on the specified SQL Server instance(s) and identifies the default Trace File + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Type + Used to specify the type. Valid types include: + + Action + Event + Map + Message + PredicateComparator + PredicateSource + Target + Type + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Get-DbaXEObject -SqlInstance sql2016 + + Lists all the XE Objects on the sql2016 SQL Server. + + .EXAMPLE + Get-DbaXEObject -SqlInstance sql2017 -Type Action, Event + + Lists all the XE Objects of type Action and Event on the sql2017 SQL Server. + + #> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [ValidateSet("Type", "Event", "Target", "Action", "Map", "Message", "PredicateComparator", "PredicateSource")] + [string[]]$Type, + [switch]$EnableException + ) + begin { + if ($Type) { + $join = $Type -join "','" + $where = "AND o.object_type in ('$join')" + $where.Replace("PredicateComparator", "pred_compare") + $where.Replace("PredicateSource", "pred_source") + } + $sql = "SELECT SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + p.name AS PackageName, + ObjectType = + CASE o.object_type + WHEN 'type' THEN 'Type' + WHEN 'event' THEN 'Event' + WHEN 'target' THEN 'Target' + WHEN 'pred_compare' THEN 'PredicateComparator' + WHEN 'pred_source' THEN 'PredicateSource' + WHEN 'action' THEN 'Action' + WHEN 'map' THEN 'Map' + WHEN 'message' THEN 'Message' + ELSE o.object_type + END, + o.object_type as ObjectTypeRaw, + o.name AS TargetName, + o.description as Description + FROM sys.dm_xe_packages AS p + JOIN sys.dm_xe_objects AS o ON p.guid = o.package_guid + WHERE (p.capabilities IS NULL OR p.capabilities & 1 = 0) + $where + AND (o.capabilities IS NULL OR o.capabilities & 1 = 0) + ORDER BY o.object_type + " + } + process { + foreach ($instance in $SqlInstance) { + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + try { + $server.Query($sql) | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, ObjectTypeRaw + } + catch { + Stop-Function -Message "Issue collecting trace data on $server." -Target $server -ErrorRecord $_ + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXESession.ps1 b/functions/Get-DbaXESession.ps1 new file mode 100644 index 000000000000..c9e799473ba6 --- /dev/null +++ b/functions/Get-DbaXESession.ps1 @@ -0,0 +1,118 @@ +function Get-DbaXESession { + <# + .SYNOPSIS + Gets a list of Extended Events Sessions from the specified SQL Server instance(s). + + .DESCRIPTION + Retrieves a list of Extended Events Sessions present on the specified SQL Server instance(s). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Only return specific sessions. Options for this parameter are auto-populated from the server. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESession + + .EXAMPLE + Get-DbaXESession -SqlInstance ServerA\sql987 + + Returns a custom object with ComputerName, SQLInstance, Session, StartTime, Status and other properties. + + .EXAMPLE + Get-DbaXESession -SqlInstance ServerA\sql987 | Format-Table ComputerName, SqlInstance, Session, Status -AutoSize + + Returns a formatted table displaying ComputerName, SqlInstance, Session, and Status. + + .EXAMPLE + 'ServerA\sql987','ServerB' | Get-DbaXESession + + Returns a custom object with ComputerName, SqlInstance, Session, StartTime, Status and other properties, from multiple SQL instances. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Sessions")] + [object[]]$Session, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Get-DbaXEsSession + } + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 -AzureUnsupported + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $XEStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + Write-Message -Level Verbose -Message "Getting XEvents Sessions on $instance." + + $xesessions = $XEStore.sessions + + if ($Session) { + $xesessions = $xesessions | Where-Object { $_.Name -in $Session } + } + + foreach ($x in $xesessions) { + $status = switch ($x.IsRunning) { $true { "Running" } $false { "Stopped" } } + $files = $x.Targets.TargetFields | Where-Object Name -eq Filename | Select-Object -ExpandProperty Value + + $filecollection = $remotefile = @() + + if ($files) { + foreach ($file in $files) { + if ($file -notmatch ':\\' -and $file -notmatch '\\\\') { + $directory = $server.ErrorLogPath.TrimEnd("\") + $file = "$directory\$file" + } + $filecollection += $file + $remotefile += Join-AdminUnc -servername $server.netName -filepath $file + } + } + + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Status -Value $status + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Session -Value $x.Name + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name TargetFile -Value $filecollection + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name RemoteTargetFile -Value $remotefile + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Parent -Value $server + Add-Member -Force -InputObject $x -MemberType NoteProperty -Name Store -Value $XEStore + Select-DefaultView -InputObject $x -Property ComputerName, InstanceName, SqlInstance, Name, Status, StartTime, AutoStart, State, Targets, TargetFile, Events, MaxMemory, MaxEventSize + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXESessionTarget.ps1 b/functions/Get-DbaXESessionTarget.ps1 new file mode 100644 index 000000000000..d7889ee7152e --- /dev/null +++ b/functions/Get-DbaXESessionTarget.ps1 @@ -0,0 +1,127 @@ +function Get-DbaXESessionTarget { + <# + .SYNOPSIS + Get a list of Extended Events Session Targets from the specified SQL Server instance(s). + + .DESCRIPTION + Retrieves a list of Extended Events Session Targets from the specified SQL Server instance(s). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Only return a specific session. Options for this parameter are auto-populated from the server. + + .PARAMETER Target + Only return a specific target. + + .PARAMETER InputObject + Specifies an XE session returned by Get-DbaXESession to search. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTarget + + .EXAMPLE + Get-DbaXESessionTarget -SqlInstance ServerA\sql987 -Session system_health + + Shows targets for the system_health session on ServerA\sql987. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session system_health | Get-DbaXESessionTarget + + Returns the targets for the system_health session on sql2016. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session system_health | Get-DbaXESessionTarget -Target package0.event_file + + Return only the package0.event_file target for the system_health session on sql2016. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(ValueFromPipeline, ParameterSetName = "instance", Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Session, + [string[]]$Target, + [parameter(ValueFromPipeline, ParameterSetName = "piped", Mandatory)] + [Microsoft.SqlServer.Management.XEvent.Session[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + function Get-Target { + [CmdletBinding()] + param ( + $Sessions, + $Session, + $Server, + $Target + ) + + foreach ($xsession in $Sessions) { + + if ($null -eq $server) { + $server = $xsession.Parent + } + + if ($Session -and $xsession.Name -notin $Session) { continue } + $status = switch ($xsession.IsRunning) { $true { "Running" } $false { "Stopped" } } + $sessionname = $xsession.Name + + foreach ($xtarget in $xsession.Targets) { + if ($Target -and $xtarget.Name -notin $Target) { continue } + + $files = $xtarget.TargetFields | Where-Object Name -eq Filename | Select-Object -ExpandProperty Value + + $filecollection = $remotefile = @() + + if ($files) { + foreach ($file in $files) { + if ($file -notmatch ':\\' -and $file -notmatch '\\\\') { + $directory = $server.ErrorLogPath.TrimEnd("\") + $file = "$directory\$file" + } + $filecollection += $file + $remotefile += Join-AdminUnc -servername $server.netName -filepath $file + } + } + + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name Session -Value $sessionname + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name SessionStatus -Value $status + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name TargetFile -Value $filecollection + Add-Member -Force -InputObject $xtarget -MemberType NoteProperty -Name RemoteTargetFile -Value $remotefile + + Select-DefaultView -InputObject $xtarget -Property ComputerName, InstanceName, SqlInstance, Session, SessionStatus, Name, ID, 'TargetFields as Field', PackageName, 'TargetFile as File', Description, ScriptName + } + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaXESession -SqlInstance $instance -SqlCredential $SqlCredential -Session $Session + } + Get-Target -Sessions $InputObject -Session $Session -Target $Target + } +} \ No newline at end of file diff --git a/functions/Get-DbaXESessionTargetFile.ps1 b/functions/Get-DbaXESessionTargetFile.ps1 new file mode 100644 index 000000000000..a878b4d56905 --- /dev/null +++ b/functions/Get-DbaXESessionTargetFile.ps1 @@ -0,0 +1,89 @@ +function Get-DbaXESessionTargetFile { + <# + .SYNOPSIS + Get a file system object from the Extended Events Session Target Files. + + .DESCRIPTION + Get a file system object from the Extended Events Session Target Files. + + Note: this performs a Get-ChildItem on remote servers if the specified target SQL Server is remote. + + .PARAMETER SqlInstance + The target SQL Server + + .PARAMETER SqlCredential + Login to SQL instnace with alternative credentials + + .PARAMETER Session + Only return files from a specific session. Options for this parameter are auto-populated from the server. + + .PARAMETER Target + Only return files from a specific target. + + .PARAMETER InputObject + Allows results from piping in Get-DbaXESessionTarget. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTargetFile + + .EXAMPLE + Get-DbaXESessionTargetFile -SqlInstance sql2017 -Session 'Long Running Queries' + + Shows Target Files for the 'Long Running Queries' session on sql2017. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2016 -Session 'Long Running Queries' | Get-DbaXESessionTarget | Get-DbaXESessionTargetFile + + Returns the Target Files for the system_health session on sql2016. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + param ( + [parameter(ValueFromPipeline, ParameterSetName = "instance", Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Session, + [string[]]$Target, + [parameter(ValueFromPipeline, ParameterSetName = "piped", Mandatory)] + [Microsoft.SqlServer.Management.XEvent.Target[]]$InputObject, + [switch]$EnableException + ) + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaXESessionTarget -SqlInstance $instance -SqlCredential $SqlCredential -Session $Session -Target $Target | Where-Object File -ne $null + } + + foreach ($object in $InputObject) { + $computer = [dbainstance]$object.ComputerName + try { + if ($computer.IsLocal) { + $file = $object.TargetFile + Write-Message -Level Verbose -Message "Getting $file" + Get-ChildItem "$file*" -ErrorAction Stop + } + else { + $file = $object.RemoteTargetFile + Write-Message -Level Verbose -Message "Getting $file" + Get-ChildItem -Recurse "$file*" -ErrorAction Stop + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXESessionTemplate.ps1 b/functions/Get-DbaXESessionTemplate.ps1 new file mode 100644 index 000000000000..d5e0d6fb1225 --- /dev/null +++ b/functions/Get-DbaXESessionTemplate.ps1 @@ -0,0 +1,137 @@ +function Get-DbaXESessionTemplate { + <# + .SYNOPSIS + Parses Extended Event XML templates. Defaults to parsing templates in the dbatools template repository (\bin\xetemplates\). + + .DESCRIPTION + Parses Extended Event XML templates. Defaults to parsing templates in the dbatools template repository (\bin\xetemplates\). + + The default repository contains templates from: + Microsoft's Templates that come with SSMS + Jes Borland's "Everyday Extended Events" presentation and GitHub repository (https://github.com/grrlgeek/extended-events) + Christian Gräfe's XE Repo: https://github.com/chrgraefe/sqlscripts/blob/master/XE-Events/ + Erin Stellato's Blog: https://www.sqlskills.com/blogs/erin/ + + Some profile templates converted using: + sp_SQLskills_ConvertTraceToExtendedEvents.sql + Jonathan M. Kehayias, SQLskills.com + http://sqlskills.com/blogs/jonathan + + .PARAMETER Path + The path to the template directory. Defaults to the dbatools template repository (\bin\xetemplates\). + + .PARAMETER Pattern + Specify a pattern for filtering. Alternatively, you can use Out-GridView -Passthru to select objects and pipe them to Import-DbaXESessionTemplate + + .PARAMETER Template + Specifies one or more of the templates provided by dbatools. Press tab to cycle through the list of options. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXESessionTemplate + + .EXAMPLE + Get-DbaXESessionTemplate + + Returns information about all the templates in the local dbatools repository. + + .EXAMPLE + Get-DbaXESessionTemplate | Out-GridView -PassThru | Import-DbaXESessionTemplate -SqlInstance sql2017 | Start-DbaXESession + + Allows you to select a Session template, then import it to the specified instance and start the session. + + .EXAMPLE + Get-DbaXESessionTemplate -Path "$home\Documents\SQL Server Management Studio\Templates\XEventTemplates" + + Returns information about all the templates in your local XEventTemplates repository. + + .EXAMPLE + Get-DbaXESessionTemplate -Pattern duration + + Returns information about all the templates that match the word "duration" in the title, category or body. + + .EXAMPLE + Get-DbaXESessionTemplate | Select-Object * + + Returns more information about the template, including the full path/filename. + #> + + [CmdletBinding()] + param ( + [string[]]$Path = "$script:PSModuleRoot\bin\xetemplates", + [string]$Pattern, + [string[]]$Template, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\xetemplates-metadata.xml" + # In case people really want a "like" search, which is slower + $Pattern = $Pattern.Replace("*", ".*").Replace("..*", ".*") + } + process { + foreach ($directory in $Path) { + $files = Get-ChildItem "$directory\*.xml" + + if ($Template) { + $files = $files | Where-Object BaseName -in $Template + } + + foreach ($file in $files) { + try { + $xml = [xml](Get-Content $file) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + foreach ($session in $xml.event_sessions) { + $meta = $metadata | Where-Object Name -eq $session.event_session.name + if ($Pattern) { + if ( + # There's probably a better way to do this + ($session.event_session.name -match $Pattern) -or + ($session.event_session.TemplateCategory.'#text' -match $Pattern) -or + ($session.event_session.TemplateSource -match $Pattern) -or + ($session.event_session.TemplateDescription.'#text' -match $Pattern) -or + ($session.event_session.TemplateName.'#text' -match $Pattern) -or + ($meta.Source -match $Pattern) + ) { + [pscustomobject]@{ + Name = $session.event_session.name + Category = $session.event_session.TemplateCategory.'#text' + Source = $meta.Source + Compatibility = ("$($meta.Compatibility)").ToString().Replace(",", "") + Description = $session.event_session.TemplateDescription.'#text' + TemplateName = $session.event_session.TemplateName.'#text' + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, TemplateName, Path + } + } + else { + [pscustomobject]@{ + Name = $session.event_session.name + Category = $session.event_session.TemplateCategory.'#text' + Source = $meta.Source + Compatibility = $meta.Compatibility.ToString().Replace(",", "") + Description = $session.event_session.TemplateDescription.'#text' + TemplateName = $session.event_session.TemplateName.'#text' + Path = $file + File = $file.Name + } | Select-DefaultView -ExcludeProperty File, TemplateName, Path + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXESmartTarget.ps1 b/functions/Get-DbaXESmartTarget.ps1 new file mode 100644 index 000000000000..e40ef5892f14 --- /dev/null +++ b/functions/Get-DbaXESmartTarget.ps1 @@ -0,0 +1,43 @@ +function Get-DbaXESmartTarget { + <# + .SYNOPSIS + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + .DESCRIPTION + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/Get-DbaXESmartTarget + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + Get-DbaXESmartTarget + + Gets an XESmartTarget PowerShell Job created by Start-DbaXESmartTarget. + + #> + [CmdletBinding()] + param ( + [switch]$EnableException + ) + process { + try { + Get-Job | Where-Object Name -Match SmartTarget | Select-Object -Property ID, Name, State + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXEStore.ps1 b/functions/Get-DbaXEStore.ps1 new file mode 100644 index 000000000000..0aaac208bd14 --- /dev/null +++ b/functions/Get-DbaXEStore.ps1 @@ -0,0 +1,64 @@ +function Get-DbaXEStore { + <# + .SYNOPSIS + Get a Extended Events store + + .DESCRIPTION + Get a Extended Events store + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbaXEStore + + .EXAMPLE + Get-DbaXEStore -SqlInstance ServerA\sql987 + + Returns an XEvent Store. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name ComputerName -Value $server.NetName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name InstanceName -Value $server.ServiceName + Add-Member -Force -InputObject $store -MemberType NoteProperty -Name SqlInstance -Value $server.DomainInstanceName + Select-DefaultView -InputObject $store -Property ComputerName, InstanceName, SqlInstance, ServerName, Sessions, Packages, RunningSessionCount + } + } +} \ No newline at end of file diff --git a/functions/Get-DbaXEventsSession.ps1 b/functions/Get-DbaXEventsSession.ps1 deleted file mode 100644 index 707bc4ef14cc..000000000000 --- a/functions/Get-DbaXEventsSession.ps1 +++ /dev/null @@ -1,127 +0,0 @@ -function Get-DbaXEventsSession -{ - <# -.SYNOPSIS -Get a list of Extended Events Sessions - -.DESCRIPTION -Retrieves a list of Extended Events Sessions - -.PARAMETER SqlInstance -The SQL Instances that you're connecting to. - -.PARAMETER SqlCredential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Sessions -Only return specific sessions. This parameter is auto-populated. - -.NOTES -Tags: Memory -Author: Klaas Vandenberghe ( @PowerDBAKlaas ) - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . - - -.LINK -https://dbatools.io/Get-DbaXEventsSession - -.EXAMPLE -Get-DbaXEventsSession -SqlServer ServerA\sql987 - -Returns a custom object with ComputerName, SQLInstance, Session, StartTime, Status and other properties. - -.EXAMPLE -Get-DbaXEventsSession -SqlServer ServerA\sql987 | Format-Table ComputerName, SQLInstance, Session, Status -AutoSize - -Returns a formatted table displaying ComputerName, SQLInstance, Session, and Status. - -.EXAMPLE -'ServerA\sql987','ServerB' | Get-DbaXEventsSession - -Returns a custom object with ComputerName, SQLInstance, Session, StartTime, Status and other properties, from multiple SQL Instances. -#> - [CmdletBinding()] - param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [string[]]$SqlInstance, - [PsCredential]$SqlCredential - ) - - DynamicParam { if ($SqlInstance) { return (Get-ParamSqlExtendedEvents -SqlServer $SqlInstance[0] -SqlCredential $SqlCredential) } } - - BEGIN - { - if ([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.XEvent") -eq $null) - { - throw "SMO version is too old. To collect Extended Events, you must have SQL Server Management Studio 2012 or higher installed." - } - $sessions = $psboundparameters.Sessions - } - PROCESS - { - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance." - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential -ErrorAction SilentlyContinue - Write-Verbose "SQL Instance $instance is version $($server.versionmajor)." - } - catch - { - Write-Warning " Failed to connect to $instance." - continue - } - if ($server.versionmajor -lt 11) - { - Write-Warning "$instance is lower than SQL Server 2012 and does not support extended events." - continue - } - else - { - $SqlConn = $server.ConnectionContext.SqlConnectionObject - $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn - $XEStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection - Write-Verbose "Getting XEvents Sessions on $instance." - - $xesessions = $XEStore.sessions - - if ($sessions) - { - $xesessions = $xesessions | Where-Object { $_.Name -in $sessions } - } - - try - { - $xesessions | - ForEach-Object { - [PSCustomObject]@{ - ComputerName = $server.NetName - SQLInstance = $server.ServiceName - Session = $_.Name - Status = switch ($_.IsRunning) { $true { "Running" } $false { "Stopped" } } - StartTime = $_.StartTime - AutoStart = $_.AutoStart - State = $_.State - Targets = $_.Targets - Events = $_.Events - MaxMemory = $_.MaxMemory - MaxEventSize = $_.MaxEventSize - } - } - } - catch - { - Write-Warning "Failed to get XEvents Sessions on $instance." - } - } - } - } - END { } -} diff --git a/functions/Get-DbatoolsLog.ps1 b/functions/Get-DbatoolsLog.ps1 new file mode 100644 index 000000000000..1feb31ffe34f --- /dev/null +++ b/functions/Get-DbatoolsLog.ps1 @@ -0,0 +1,47 @@ +function Get-DbatoolsLog { + <# + .SYNOPSIS + Returns log entries for dbatools + + .DESCRIPTION + Returns log entries for dbatools. Handy when debugging or developing for it. Also used when preparing a support package. + + .PARAMETER Errors + Instead of log entries, the error entries will be retrieved + + .NOTES + Author: Fred Weinmann (@FredWeinmann) + Tags: Debug + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Get-DbatoolsLog + + .EXAMPLE + Get-DbatoolsLog + + Returns all log entries currently in memory. + #> + [CmdletBinding()] + param + ( + [switch] + $Errors + ) + + BEGIN { + # No Write-Message, since that would clutter the very log you want to retrieve + } + + PROCESS { + if ($Errors) { return [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::GetErrors() } + else { return [Sqlcollaborative.Dbatools.dbaSystem.DebugHost]::GetLog() } + } + + END { + + } +} diff --git a/functions/Get-SqlRegisteredServerName.ps1 b/functions/Get-SqlRegisteredServerName.ps1 deleted file mode 100644 index 166a7d936b85..000000000000 --- a/functions/Get-SqlRegisteredServerName.ps1 +++ /dev/null @@ -1,214 +0,0 @@ -Function Get-SqlRegisteredServerName -{ -<# -.SYNOPSIS -Gets list of SQL Server names stored in SQL Server Central Management Server - -.DESCRIPTION -Returns a simple array of server names - -.PARAMETER SqlServer -The SQL Server instance. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER Group -Auto-populated list of groups in SQL Server Central Management Server. You can specify one or more, comma separated. - -.PARAMETER NoCmsServer -By default, the Central Management Server name is included in the list. Use -NoCmsServer to exclude the CMS itself. - -.PARAMETER NetBiosName -Returns just the NetBios names of each server - -.PARAMETER IpAddr -Returns just the ip addresses of each server - -.NOTES -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Get-SqlRegisteredServerName - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sqlserver2014a - -Gets a list of all server names from the Central Management Server on sqlserver2014a, using Windows Credentials - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sqlserver2014a -SqlCredential $credential - -Gets a list of all server names from the Central Management Server on sqlserver2014a, using SQL Authentication - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sqlserver2014a -Groups HR, Accounting - -Gets a list of server names in the HR and Accounting groups from the Central Management Server on sqlserver2014a. - -.EXAMPLE -Get-SqlRegisteredServerName -SqlServer sqlserver2014a -Groups HR, Accounting -IpAddr - -Gets a list of server IP addresses in the HR and Accounting groups from the Central Management Server on sqlserver2014a. - -#> - [CmdletBinding(DefaultParameterSetName = "Default")] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [object]$SqlCredential, - [switch]$NoCmsServer, - [parameter(ParameterSetName = "NetBios")] - [switch]$NetBiosName, - [parameter(ParameterSetName = "IP")] - [switch]$IpAddr - ) - - DynamicParam { if ($sqlserver) { return Get-ParamSqlCmsGroups -SqlServer $sqlserver -SqlCredential $SqlCredential } } - - BEGIN - { - $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential - $sqlconnection = $server.ConnectionContext.SqlConnectionObject - - try - { - $cmstore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sqlconnection) - } - catch - { - throw "Cannot access Central Management Server" - } - - $groups = $psboundparameters.Group - } - - PROCESS - { - - # see notes at Get-ParamSqlCmsGroups - Function Find-CmsGroup($CmsGrp, $base = '', $stopat) - { - $results = @() - foreach($el in $CmsGrp) { - if($base -eq ''){ - $partial = $el.name - } else { - $partial = "$base\$($el.name)" - } - if ($partial -eq $stopat) { - return $el - } else { - foreach($group in $el.ServerGroups) { - $results += Find-CmsGroup $group $partial $stopat - } - } - } - return $results - } - - $servers = @() - if ($groups -ne $null) - { - foreach ($group in $groups) - { - $cms = Find-CmsGroup $cmstore.DatabaseEngineServerGroup.ServerGroups '' $group - $servers += ($cms.GetDescendantRegisteredServers()).servername - } - } - else - { - $cms = $cmstore.ServerGroups["DatabaseEngineServerGroup"] - $servers = ($cms.GetDescendantRegisteredServers()).servername - } - - if ($NoCmsServer -eq $false) - { - $servers += $sqlserver - } - } - - END - { - $server.ConnectionContext.Disconnect() - - if ($NetBiosName -or $IpAddr) - { - $ipcollection = @() - $netbioscollection = @() - $processed = @() - - foreach ($server in $servers) - { - if ($server -match '\\') - { - $server = $server.Split('\')[0] - } - - if ($processed -contains $server) { continue } - $processed += $server - - try - { - Write-Verbose "Testing connection to $server and resolving IP address" - $ipaddress = ((Test-Connection $server -Count 1 -ErrorAction SilentlyContinue).Ipv4Address | Select-Object -First 1).IPAddressToString - } - catch - { - Write-Warning "Could not resolve IP address for $server" - continue - } - - if ($ipcollection -notcontains $ipaddress) { $ipcollection += $ipaddress } - - if ($NetBiosName) - { - try - { - $hostname = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=TRUE -ComputerName $ipaddress -ErrorAction SilentlyContinue).PSComputerName - if ($hostname -is [array]) { $hostname = $hostname[0] } - Write-Verbose "Hostname resolved to $hostname" - if ($hostname -eq $null) { $hostname = (nbtstat -A $ipaddress | Where-Object { $_ -match '\<00\> UNIQUE' } | ForEach-Object { $_.SubString(4, 14) }).Trim() } - } - catch - { - Write-Warning "Could not resolve NetBios name for $server" - continue - } - - if ($netbioscollection -notcontains $hostname) { $netbioscollection += $hostname } - } - } - - if ($NetBiosName) - { - return $netbioscollection - } - else - { - return $ipcollection - } - } - else - { - return $servers - } - } -} diff --git a/functions/Get-SqlServerKey.ps1 b/functions/Get-SqlServerKey.ps1 deleted file mode 100644 index 929fb22dcd26..000000000000 --- a/functions/Get-SqlServerKey.ps1 +++ /dev/null @@ -1,251 +0,0 @@ -Function Get-SqlServerKey -{ -<# -.SYNOPSIS -Gets SQL Server Product Keys from local on destination SQL Servers. Works with SQL Server 2005-2014 - -.DESCRIPTION -Using a string of servers, a text file, or Central Management Server to provide a list of servers, this script will go to each server and get the product key for all installed instances. Clustered instances are supported as well. Requires regular user access to the SQL instances, SMO installed locally, Remote Registry enabled and acessible by the account running the script. - -Uses key decoder by Jakob Bindslet (http://goo.gl/1jiwcB) - -.PARAMETER SqlServers -A comma separated list of servers. This can be the NetBIOS name, IP, or SQL instance name - -.PARAMETER SqlCms -Compiles list of servers to inventory using all servers stored within a Central Management Server. Requires having SQL Management Studio installed. - -.PARAMETER ServersFromFile -Uses a text file as input. The file must be formatted as such: -sqlserver1 -sqlserver2 - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$cred = Get-Credential, this pass this $cred to the param. - -Windows Authentication will be used if DestinationSqlCredential is not specified. To connect as a different Windows user, run PowerShell as that user. - -.NOTES -Author: Chrissy LeMaire (@cl), netnerds.net -Requires: PowerShell Version 3.0, SQL Server SMO, Remote Registry -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Get-SqlServerKey - -.EXAMPLE -Get-SqlServerKey winxp, sqlservera, sqlserver2014a, win2k8 -Gets SQL Server versions, editions and product keys for all instances within each server or workstation. - -.EXAMPLE -Get-SqlServerKey -SqlCms sqlserver01 -Gets SQL Server versions, editions and product keys for all instances within sqlserver01's Central Management Server - -.EXAMPLE -Get-SqlServerKey -ServersFromFile C:\Scripts\servers.txt -Gets SQL Server versions, editions and product keys for all instances listed within C:\Scripts\servers.txt -#> - [CmdletBinding(DefaultParameterSetName = "Default")] - Param ( - [parameter(Position = 0)] - [Alias("ServerInstance","SqlInstance")] - [string[]]$SqlServers, - # Central Management Server - - [string]$SqlCms, - # File with one server per line - - [string]$ServersFromFile, - [System.Management.Automation.PSCredential]$SqlCredential - ) - - BEGIN - { - - Function Unlock-SqlServerKey - { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [byte[]]$data, - [int]$version - ) - try - { - if ($version -ge 11) { $binArray = ($data)[0..66] } - else { $binArray = ($data)[52..66] } - $charsArray = "B", "C", "D", "F", "G", "H", "J", "K", "M", "P", "Q", "R", "T", "V", "W", "X", "Y", "2", "3", "4", "6", "7", "8", "9" - for ($i = 24; $i -ge 0; $i--) - { - $k = 0 - for ($j = 14; $j -ge 0; $j--) - { - $k = $k * 256 -bxor $binArray[$j] - $binArray[$j] = [math]::truncate($k / 24) - $k = $k % 24 - } - $productKey = $charsArray[$k] + $productKey - if (($i % 5 -eq 0) -and ($i -ne 0)) - { - $productKey = "-" + $productKey - } - } - } - catch { $productkey = "Cannot decode product key." } - return $productKey - } - } - - PROCESS - { - - if ($SqlCms) - { - if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.RegisteredServers") -eq $null) - { throw "Can't load CMS assemblies. You must have SQL Server Management Studio installed to use the -SqlCms switch." } - - Write-Output "Gathering SQL Servers names from Central Management Server" - $server = Connect-SqlServer -SqlServer $SqlCms -SqlCredential $SqlCredential - $sqlconnection = $server.ConnectionContext.SqlConnectionObject - - try { $cmstore = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sqlconnection) } - catch { throw "Cannot access Central Management Server" } - $dbstore = $cmstore.DatabaseEngineServerGroup - $SqlServers = $dbstore.GetDescendantRegisteredServers().servername - # Add the CM server itself, which can't be stored in the CM server. - $servers += $SqlCms - $basenames = @() - foreach ($server in $SqlServers) { $basenames += $server.Split("\")[0] } - $SqlServers = $basenames | Get-Unique - } - - If ($ServersFromFile) - { - if ((Test-Path $ServersFromFile) -eq $false) { throw "Could not find file: $ServersFromFile" } - $SqlServers = Get-Content $ServersFromFile - } - - if ([string]::IsNullOrEmpty($SqlServers)) { $SqlServers = $env:computername } - - $basepath = "SOFTWARE\Microsoft\Microsoft SQL Server" - # Loop through each server - $objectCollection = @() - foreach ($servername in $SqlServers) - { - $servername = $servername.Split("\")[0] - - if ($servername -eq "." -or $servername -eq "localhost" -or $servername -eq $env:computername) - { - $localmachine = [Microsoft.Win32.RegistryHive]::LocalMachine - $defaultview = [Microsoft.Win32.RegistryView]::Default - $reg = [Microsoft.Win32.RegistryKey]::OpenBaseKey($localmachine, $defaultview) - } - else - { - # Get IP for remote registry access. It's the most reliable. - try { $ipaddr = ([System.Net.Dns]::GetHostAddresses($servername)).IPAddressToString } - catch { Write-Warning "Can't resolve $servername. Skipping."; continue } - - try - { - $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $ipaddr) - } - catch { Write-Warning "Can't access registry for $servername. Is the Remote Registry service started?"; continue } - } - - $instances = $reg.OpenSubKey("$basepath\Instance Names\SQL", $false) - if ($instances -eq $null) { Write-Warning "No instances found on $servername. Skipping."; continue } - # Get Product Keys for all instances on the server. - foreach ($instance in $instances.GetValueNames()) - { - if ($instance -eq "MSSQLSERVER") { $sqlserver = $servername } - else { $sqlserver = "$servername\$instance" } - - $subkeys = $reg.OpenSubKey("$basepath", $false) - $instancekey = $subkeys.GetSubKeynames() | Where-Object { $_ -like "*.$instance" } - if ($instancekey -eq $null) { $instancekey = $instance } # SQL 2k5 - - # Cluster instance hostnames are required for SMO connection - $cluster = $reg.OpenSubKey("$basepath\$instancekey\Cluster", $false) - if ($cluster -ne $null) - { - $clustername = $cluster.GetValue("ClusterName") - if ($instance -eq "MSSQLSERVER") { $sqlserver = $clustername } - else { $sqlserver = "$clustername\$instance" } - } - - Write-Verbose "Attempting to connect to $sqlserver" - try { $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential } - catch { Write-Warning "Can't connect to $sqlserver or access denied. Skipping."; continue } - - $servicePack = $server.ProductLevel - Write-Debug "$servername $instance version is $($server.VersionMajor)" - switch ($server.VersionMajor) - { - 9 { - $sqlversion = "SQL Server 2005 $servicePack" - $findkeys = $reg.OpenSubKey("$basepath\90\ProductID", $false) - foreach ($findkey in $findkeys.GetValueNames()) - { - if ($findkey -like "DigitalProductID*") { $key = "$basepath\90\ProductID\$findkey" } - } - } - 10 { - $sqlversion = "SQL Server 2008 $servicePack" - $key = "$basepath\MSSQL10" - if ($server.VersionMinor -eq 50) { $key += "_50"; $sqlversion = "SQL Server 2008 R2 $servicePack" } - $key += ".$instance\Setup\DigitalProductID" - } - 11 { $key = "$basepath\110\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2012 $servicePack" } - 12 { $key = "$basepath\120\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2014 $servicePack" } - 13 { $key = "$basepath\130\Tools\Setup\DigitalProductID"; $sqlversion = "SQL Server 2016 $servicePack" } - default { Write-Warning "SQL version not currently supported."; continue } - } - if ($server.Edition -notlike "*Express*") - { - try - { - $subkey = Split-Path $key; $binaryvalue = Split-Path $key -leaf - $binarykey = $($reg.OpenSubKey($subkey)).GetValue($binaryvalue) - } - catch { $sqlkey = "Could not connect." } - try { $sqlkey = Unlock-SqlServerKey $binarykey $server.VersionMajor } - catch { } - } - else { $sqlkey = "SQL Server Express Edition" } - $server.ConnectionContext.Disconnect() - - $object = New-Object PSObject -Property @{ - "SQL Instance" = $sqlserver - "SQL Version" = $sqlversion - "SQL Edition" = $server.Edition - "Product Key" = $sqlkey - } - $objectCollection += $object - } - $reg.Close() - } - $objectCollection | Select "SQL Instance", "SQL Version", "SQL Edition", "Product Key" - } - - END - { - # Write-Output "Script completed" - } -} diff --git a/functions/Import-DbaCsvToSql.ps1 b/functions/Import-DbaCsvToSql.ps1 new file mode 100644 index 000000000000..3bb7cad8e7ca --- /dev/null +++ b/functions/Import-DbaCsvToSql.ps1 @@ -0,0 +1,1348 @@ +function Import-DbaCsvToSql { + <# + .SYNOPSIS + Efficiently imports very large (and small) CSV files into SQL Server using only the .NET Framework and PowerShell. + + .DESCRIPTION + Import-DbaCsvToSql takes advantage of .NET's super fast SqlBulkCopy class to import CSV files into SQL Server at up to 90,000 rows a second. + + The entire import is contained within a transaction, so if a failure occurs or the script is aborted, no changes will persist. + + If the table specified does not exist, it will be automatically created using best guessed data types. In addition, the destination table can be truncated prior to import. + + The Query parameter will be used to import only the data returned from a SQL Query executed against the CSV file(s). This function supports a number of bulk copy options. Please see parameter list for details. + + .PARAMETER CSV + Specifies path to the CSV file(s) to be imported. Multiple files may be imported if they are formatted similarly. + + If no file is specified, a dialog box will appear to select your file(s). + + .PARAMETER FirstRowColumns + If this switch is enabled, the first row in the file will be used as column names for the data being imported. + + If the first row does not contain column names and -Query is specified, use field names "column1, column2, column3" and so on. + + .PARAMETER Delimiter + Specifies the delimiter used in the imported file(s). If no delimiter is specified, comma is assumed. + + Valid delimiters are '`t`, '|', ';',' ' and ',' (tab, pipe, semicolon, space, and comma). + + .PARAMETER SingleColumn + Specifies that the file contains a single column of data + + .PARAMETER SqlInstance + The SQL Server Instance to import data into. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database the CSV will be imported into. Options for this this parameter are auto-populated from the server. + + .PARAMETER Schema + Specifies the schema in which the SQL table or view where CSV will be imported into resides. Default is dbo + + If a schema name is not specified, and a CSV name with multiple dots is specified (ie; something.data.csv) then this will be interpreted as a request to import into a table [data] in the schema [something]. + + If a schema does not currently exist, it will be created, after a prompt to confirm this. Authorization will be set to dbo by default + + .PARAMETER Table + Specifies the SQL table or view where CSV will be imported into. + + If a table name is not specified, the table name will be automatically determined from the filename, and a prompt will appear to confirm the table name. + + If a table does not currently exist, it will created. SQL datatypes are determined from the first row of the CSV that contains data (skips first row if -FirstRowColumns is specified). Datatypes used are: bigint, numeric, datetime and varchar(MAX). + + If the automatically generated table datatypes do not work for you, please create the table prior to import. + + .PARAMETER Truncate + If this switch is enabled, the destination table will be truncated prior to import. + + .PARAMETER Safe + If this switch is enabled, OleDb is used to import the records. By default, Import-DbaCsvToSql uses StreamReader for imports. StreamReader is super fast, but may not properly parse some files. + + When using OleDb the import will be slower but more predictable when it comes to parsing CSV files. A schema.ini is automatically generated for best results. If schema.ini currently exists in the directory, it will be moved to a temporary location, then moved back. + + OleDB also enables the script to use the -Query parameter, which enables you to import specific subsets of data within a CSV file. OleDB imports at up to 21,000 rows/sec. + + .PARAMETER Turbo + If this switch is enabled, a Table Lock will be created for the import to make the import run as fast as possible. Depending upon the number of columns and datatypes, this may be over 90,000 records per second. + + This switch cannot be used in conjunction with -Query. + + Remember the Turbo button? This one actually works. Turbo is mega fast, but may not handle some datatypes as well as other methods. + + If your CSV file is rather vanilla and doesn't have a ton of NULLs, Turbo may work well for you. + + .PARAMETER First + Specifies the number of rows to import. If this parameter is omitted, the entire file is imported. Row counts start at the top of the file, but skip the first row if -FirstRowColumns is specified. + + Use -Query if you need advanced First (TOP) functionality. + + .PARAMETER Query + Specifies a query to execute against the CSV data to select/modify the data being imported. + + To make command line queries easy, this module will convert the word "csv" to the actual CSV formatted table name. If the FirstRowColumns switch is not used, the query should use column1, column2, column3, etc. + + Cannot be used in conjunction with -Turbo or -First. When -Query is specified, the slower import method, OleDb, will be used. + + .PARAMETER NotifyAfter + Specifies the import row count interval for reporting progress. A notification will be shown after each group of this many rows has been imported. + + .PARAMETER BatchSize + Specifies the batch size for the import. Defaults to 50000. + + .PARAMETER TableLock + If this switch is enabled, the SqlBulkCopy option to acquire a table lock will be used. This is automatically used if -Turbo is enabled. + + Per Microsoft "Obtain a bulk update lock for the duration of the bulk copy operation. When not + specified, row locks are used." + + .PARAMETER CheckConstraints + If this switch is enabled, the SqlBulkCopy option to check constraints will be used. + + Per Microsoft "Check constraints while data is being inserted. By default, constraints are not checked." + + .PARAMETER FireTriggers + If this switch is enabled, the SqlBulkCopy option to allow insert triggers to be executed will be used. + + Per Microsoft "When specified, cause the server to fire the insert triggers for the rows being inserted into the database." + + .PARAMETER KeepIdentity + If this switch is enabled, the SqlBulkCopy option to keep identity values from the source will be used. + + Per Microsoft "Preserve source identity values. When not specified, identity values are assigned by the destination." + + .PARAMETER KeepNulls + If this switch is enabled, the SqlBulkCopy option to keep NULL values in the table will be used. + + Per Microsoft "Preserve null values in the destination table regardless of the settings for default values. When not specified, null values are replaced by default values where applicable." + + .NOTES + Tags: Migration + Author: Chrissy LeMaire (@cl), netnerds.net + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://blog.netnerds.net/2015/09/Import-DbaCsvtosql-super-fast-csv-to-sql-server-import-powershell-module/ + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv to the SQL "markets" database on a SQL Server named sql001. + + Since a table name was not specified, the table name is automatically determined from filename as "housing" and a prompt will appear to confirm table name. + + The first row is not skipped, as it does not contain column names. + + .EXAMPLE + Import-DbaCsvToSql -Csv .\housing.csv -SqlInstance sql001 -Database markets -Table housing -First 100000 -Safe -Delimiter "`t" -FirstRowColumns + + Imports the first 100,000 rows of the tab delimited housing.csv file to the "housing" table in the "markets" database on a SQL Server named sql001. Since -Safe was specified, the OleDB method will be used for the bulk import. The first row is skipped, as it contains column names. + + .EXAMPLE + Import-DbaCsvToSql -csv C:\temp\huge.txt -SqlInstance sqlcluster -Database locations -Table latitudes -Delimiter "|" -Turbo + + Imports all records from the pipe delimited huge.txt file using the fastest method possible into the latitudes table within the locations database. Obtains a table lock for the duration of the bulk copy operation. This specific command has been used + to import over 10.5 million rows in 2 minutes. + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv, .\housing2.csv -SqlInstance sql001 -Database markets -Table housing -Delimiter "`t" -query "select top 100000 column1, column3 from csv" -Truncate + + Truncates the "housing" table, then imports columns 1 and 3 of the first 100000 rows of the tab-delimited housing.csv in the C:\temp directory, and housing2.csv in the current directory. Since the query is executed against both files, a total of 200,000 rows will be imported. + + .EXAMPLE + Import-DbaCsvToSql -Csv C:\temp\housing.csv -SqlInstance sql001 -Database markets -Table housing -query "select address, zip from csv where state = 'Louisiana'" -FirstRowColumns -Truncate -FireTriggers + + Uses the first line to determine CSV column names. Truncates the "housing" table on the SQL Server, then imports the address and zip columns from all records in the housing.csv where the state equals Louisiana. + + Triggers are fired for all rows. Note that this does slightly slow down the import. + + .EXAMPLE + Import-DbaCsvToSql -Csv c:\temp\SingleColumn.csv -SqlInstance sql001 -Database markets -Table TempTable -SingleColumn + + Upload the single column Csv SingleColumn.csv to Temptable which has just one column + + .EXAMPLE + Import-DbaCsvToSql -Csv "\\FileServer\To Import\housing.csv" -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv located in the share named "To Import" on FileServer to the SQL "markets" database on a SQL Server named sql001. + + .EXAMPLE + Import-DbaCsvToSql -Csv '\\FileServer\R$\To Import\housing.csv' -SqlInstance sql001 -Database markets + + Imports the entire comma-delimited housing.csv located in the directory R:\To Import on FileServer using the administrative share to the SQL "markets" database on a SQL Server named sql001. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [object]$SqlCredential, + [string]$Table, + [string]$Schema = "dbo", + [switch]$Truncate, + [ValidateSet("`t", "|", ";", " ", ",")] + [string]$Delimiter = ",", + [switch]$SingleColumn, + [switch]$FirstRowColumns, + [parameter(ParameterSetName = "reader")] + [switch]$Turbo, + [parameter(ParameterSetName = "ole")] + [switch]$Safe, + [int]$First = 0, + [parameter(ParameterSetName = "ole")] + [string]$Query = "select * from csv", + [int]$BatchSize = 50000, + [int]$NotifyAfter, + [switch]$TableLock, + [switch]$CheckConstraints, + [switch]$FireTriggers, + [switch]$KeepIdentity, + [switch]$KeepNulls, + #[Parameter(DontShow)] + [switch]$shellswitch, + #[Parameter(DontShow)] + [string]$SqlCredentialPath + ) + + DynamicParam { + + if ($SqlInstance.length -gt 0) { + # Auto populate database list from specified sqlserver + $paramconn = New-Object System.Data.SqlClient.SqlConnection + + if ($SqlCredentialPath.length -gt 0) { + $SqlCredential = Import-CliXml $SqlCredentialPath + } + + if ($SqlCredential.count -eq 0 -or $null -eq $SqlCredential) { + $paramconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;" + } + else { + $paramconn.ConnectionString = "Data Source=$SqlInstance;User Id=$($SqlCredential.UserName); Password=$($SqlCredential.GetNetworkCredential().Password);" + } + + try { + $paramconn.Open() + $sql = "select name from master.dbo.sysdatabases" + $paramcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $paramconn, $null) + $paramdt = New-Object System.Data.DataTable + $paramdt.Load($paramcmd.ExecuteReader()) + $databaselist = $paramdt.rows.name + $null = $paramcmd.Dispose() + $null = $paramconn.Close() + $null = $paramconn.Dispose() + } + catch { + # But if the routine fails, at least let them specify a database manually + $databaselist = "" + } + + # Reusable parameter setup + $newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $attributes = New-Object System.Management.Automation.ParameterAttribute + $attributes.Mandatory = $false + + # Database list parameter setup + $dbattributes = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] + $dbattributes.Add($attributes) + # If a list of databases were returned, populate the parameter set + if ($databaselist.length -gt 0) { + $dbvalidationset = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $databaselist + $dbattributes.Add($dbvalidationset) + } + + $Database = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Database", [String], $dbattributes) + $newparams.Add("Database", $Database) + return $newparams + } + } + + begin { + function Get-Columns { + <# + .SYNOPSIS + TextFieldParser will be used instead of an OleDbConnection. + This is because the OleDbConnection driver may not exist on x64. + + .EXAMPLE + $columns = Get-Columns -Csv .\myfile.csv -Delimiter "," -FirstRowColumns $true + + .OUTPUTS + Array of column names + #> + + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [Parameter(Mandatory = $true)] + [bool]$FirstRowColumns + ) + + $columnparser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($csv[0]) + $columnparser.TextFieldType = "Delimited" + $columnparser.SetDelimiters($Delimiter) + $rawcolumns = $columnparser.ReadFields() + + if ($FirstRowColumns -eq $true) { + $columns = ($rawcolumns | ForEach-Object { $_ -Replace '"' } | Select-Object -Property @{ Name = "name"; Expression = { "[$_]" } }).name + } + else { + $columns = @() + foreach ($number in 1..$rawcolumns.count) { + $columns += "[column$number]" + } + } + + $columnparser.Close() + $columnparser.Dispose() + return $columns + } + + function Get-ColumnText { + <# + .SYNOPSIS + Returns an array of data, which can later be parsed for potential datatypes. + + .EXAMPLE + $columns = Get-Columns -Csv .\myfile.csv -Delimiter "," + + .OUTPUTS + Array of column data + #> + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter + ) + $columnparser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($csv[0]) + $columnparser.TextFieldType = "Delimited" + $columnparser.SetDelimiters($Delimiter) + $line = $columnparser.ReadLine() + # Skip a line, in case first line are column names + $line = $columnparser.ReadLine() + $datatext = $columnparser.ReadFields() + $columnparser.Close() + $columnparser.Dispose() + return $datatext + } + + function Write-Schemaini { + <# + .SYNOPSIS + Unfortunately, passing delimiter within the OleDBConnection connection string is unreliable, so we'll use schema.ini instead. The default delimiter in Windows changes depending on country, so we'll do this for every delimiter, even commas. + + Get OLE datatypes based on best guess of column data within the -Columns parameter. + + Sometimes SQL will accept a datetime that OLE won't, so Text will be used for datetime. + + .EXAMPLE + $columns = Get-Columns -Csv C:\temp\myfile.csv -Delimiter "," + $movedschemainis = Write-Schemaini -Csv C:\temp\myfile.csv -Columns $columns -ColumnText $columntext -Delimiter "," -FirstRowColumns $true + + .OUTPUTS + Creates new schema files, that look something like this: + + [housingdata.csv] + Format=Delimited(,) + ColNameHeader=True + Col1="House ID" Long + Col2="Description" Memo + Col3="Price" Double + + Returns an array of existing schema files that have been moved, if any. + #> + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string[]]$Columns, + [string[]]$ColumnText, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [Parameter(Mandatory = $true)] + [bool]$FirstRowColumns + ) + + $movedschemainis = @{ } + foreach ($file in $csv) { + $directory = Split-Path $file + $schemaexists = Test-Path "$directory\schema.ini" + if ($schemaexists -eq $true) { + $newschemaname = "$env:TEMP\$(Split-Path $file -leaf)-schema.ini" + $movedschemainis.Add($newschemaname, "$directory\schema.ini") + Move-Item "$directory\schema.ini" $newschemaname -Force + } + + $filename = Split-Path $file -leaf; $directory = Split-Path $file + Add-Content -Path "$directory\schema.ini" -Value "[$filename]" + Add-Content -Path "$directory\schema.ini" -Value "Format=Delimited($InternalDelimiter)" + Add-Content -Path "$directory\schema.ini" -Value "ColNameHeader=$FirstRowColumns" + + $index = 0 + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]", '"' }) + + foreach ($datatype in $columntext) { + $olecolumnname = $olecolumns[$index] + $index++ + + try { + [System.Guid]::Parse($datatype) | Out-Null; $isguid = $true + } + catch { + $isguid = $false + } + + if ($isguid -eq $true) { + $oledatatype = "Text" + } + elseif ([int64]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Long" + } + elseif ([double]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Double" + } + elseif ([datetime]::TryParse($datatype, [ref]0) -eq $true) { + $oledatatype = "Text" + } + else { + $oledatatype = "Memo" + } + + Add-Content -Path "$directory\schema.ini" -Value "Col$($index)`=$olecolumnname $oledatatype" + } + } + return $movedschemainis + } + + function New-SqlTable { + <# + .SYNOPSIS + Creates new Table using existing SqlCommand. + + SQL datatypes based on best guess of column data within the -ColumnText parameter. + Columns parameter determine column names. + + .EXAMPLE + New-SqlTable -Csv $Csv -Delimiter $InternalDelimiter -Columns $columns -ColumnText $columntext -SqlConn $sqlconn -Transaction $transaction + + .OUTPUTS + Creates new table + #> + + param ( + [Parameter(Mandatory = $true)] + [string[]]$Csv, + [Parameter(Mandatory = $true)] + [string]$Delimiter, + [string[]]$Columns, + [string[]]$ColumnText, + [System.Data.SqlClient.SqlConnection]$sqlconn, + [System.Data.SqlClient.SqlTransaction]$transaction + ) + # Get SQL datatypes by best guess on first data row + $sqldatatypes = @(); $index = 0 + + foreach ($column in $columntext) { + $sqlcolumnname = $Columns[$index] + $index++ + + # bigint, float, and datetime are more accurate, but it didn't work + # as often as it should have, so we'll just go for a smaller datatype + if ([int64]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + elseif ([double]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + elseif ([datetime]::TryParse($column, [ref]0) -eq $true) { + $sqldatatype = "varchar(255)" + } + else { + $sqldatatype = "varchar(MAX)" + } + + $sqldatatypes += "$sqlcolumnname $sqldatatype" + } + + $sql = "BEGIN CREATE TABLE [$schema].[$table] ($($sqldatatypes -join ' NULL,')) END" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + $errormessage = $_.Exception.Message.ToString() + throw "Failed to execute $sql. `nDid you specify the proper delimiter? `n$errormessage" + } + + Write-Output "[*] Successfully created table $schema.$table with the following column definitions:`n $($sqldatatypes -join "`n ")" + # Write-Warning "All columns are created using a best guess, and use their maximum datatype." + Write-Warning "This is inefficient but allows the script to import without issues." + Write-Warning "Consider creating the table first using best practices if the data will be used in production." + } + + + if ($shellswitch -eq $false) { Write-Output "[*] Started at $(Get-Date)" } + + # Load the basics + [void][Reflection.Assembly]::LoadWithPartialName("System.Data") + [void][Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") + [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") + + # Getting the total rows copied is a challenge. Use SqlBulkCopyExtension. + # http://stackoverflow.com/questions/1188384/sqlbulkcopy-row-count-when-complete + + $source = 'namespace System.Data.SqlClient + { + using Reflection; + + public static class SqlBulkCopyExtension + { + const String _rowsCopiedFieldName = "_rowsCopied"; + static FieldInfo _rowsCopiedField = null; + + public static int RowsCopiedCount(this SqlBulkCopy bulkCopy) + { + if (_rowsCopiedField == null) _rowsCopiedField = typeof(SqlBulkCopy).GetField(_rowsCopiedFieldName, BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance); + return (int)_rowsCopiedField.GetValue(bulkCopy); + } + } + } + ' + Add-Type -ReferencedAssemblies 'System.Data.dll' -TypeDefinition $source -ErrorAction SilentlyContinue + } + + process { + # turbo mode requires a table lock, or it's just regular fast + if ($turbo -eq $true) { + $tablelock = $true + } + + # Hack to get around the delimter parameter ValidateSet + if ($SingleColumn -eq $true) { + $InternalDelimiter = '' + } + else { + $InternalDelimiter = $Delimiter + } + + # The query parameter requires OleDB which is invoked by the "safe" variable + # Actually, a select could be performed on the datatable used in StreamReader, too. + # Maybe that will be done later. + if ($query -ne "select * from csv") { + $safe = $true + } + + if ($first -gt 0 -and $query -ne "select * from csv") { + throw "Cannot use both -Query and -First. If a query is necessary, use TOP $first within your SQL statement." + } + + # In order to support -First in both Streamreader, and OleDb imports, the query must be modified slightly. + if ($first -gt 0) { + $query = "select top $first * from csv" + } + + # If shell switch occured, and encrypted SQL credentials were written to disk, create $SqlCredential + if ($SqlCredentialPath.length -gt 0) { + $SqlCredential = Import-CliXml $SqlCredentialPath + } + + # Get Database string from RuntimeDefinedParameter if required + if ($database -isnot [string]) { + $database = $PSBoundParameters.Database + } + if ($database.length -eq 0) { + throw "You must specify a database." + } + + # Check to ensure a Windows account wasn't used as a SQL Credential + if ($SqlCredential.count -gt 0 -and $SqlCredential.UserName -like "*\*") { + throw "Only SQL Logins can be used as a SqlCredential." + } + + # If no CSV was specified, prompt the user to select one. + if ($csv.length -eq 0) { + $fd = New-Object System.Windows.Forms.OpenFileDialog + $fd.InitialDirectory = [environment]::GetFolderPath("MyDocuments") + $fd.Filter = "CSV Files (*.csv;*.tsv;*.txt)|*.csv;*.tsv;*.txt" + $fd.Title = "Select one or more CSV files" + $fd.MultiSelect = $true + $null = $fd.showdialog() + $csv = $fd.filenames + if ($csv.length -eq 0) { + throw "No CSV file selected." + } + } + else { + foreach ($file in $csv) { + $exists = Test-Path $file + if ($exists -eq $false) { + throw "$file does not exist" + } + } + } + + # Resolve the full path of each CSV + $resolvedcsv = @() + foreach ($file in $csv) { + $resolvedcsv += (Resolve-Path $file).ProviderPath + } + $csv = $resolvedcsv + + # UniqueIdentifier kills OLE DB / SqlBulkCopy imports. Check to see if destination table contains this datatype. + if ($safe -eq $true) { + $sqlcheckconn = New-Object System.Data.SqlClient.SqlConnection + if ($SqlCredential.count -eq 0 -or $null -eq $SqlCredential) { + $sqlcheckconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;Connection Timeout=3; Initial Catalog=master" + } + else { + $username = ($SqlCredential.UserName).TrimStart("\") + $sqlcheckconn.ConnectionString = "Data Source=$SqlInstance;User Id=$username; Password=$($SqlCredential.GetNetworkCredential().Password);Connection Timeout=3; Initial Catalog=master" + } + + try { + $sqlcheckconn.Open() + } + catch { + throw $_.Exception + } + + # Ensure database exists + $sql = "select count(*) from master.dbo.sysdatabases where name = '$database'" + $sqlcheckcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlcheckconn) + $dbexists = $sqlcheckcmd.ExecuteScalar() + if ($dbexists -eq $false) { + throw "Database does not exist on $SqlInstance" + } + + # Change database after the fact, because if db doesn't exist, the login would fail. + $sqlcheckconn.ChangeDatabase($database) + + $sql = "SELECT t.name as datatype FROM sys.columns c + JOIN sys.types t ON t.system_type_id = c.system_type_id + WHERE c.object_id = object_id('$schema.$table') and t.name != 'sysname'" + $sqlcheckcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlcheckconn) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.load($sqlcheckcmd.ExecuteReader("CloseConnection")) + $sqlcheckconn.Dispose() + if ($sqlcolumns.datatype -contains "UniqueIdentifier") { + throw "UniqueIdentifier not supported by OleDB/SqlBulkCopy. Query and Safe cannot be supported." + } + } + + if ($safe -eq $true) { + # Check for drivers. First, ACE (Access) if file is smaller than 2GB, then JET + # ACE doesn't handle files larger than 2gb. What gives? + foreach ($file in $csv) { + $filesize = (Get-ChildItem $file).Length / 1GB + if ($filesize -gt 1.99) { + $jetonly = $true + } + } + + if ($jetonly -ne $true) { + $provider = (New-Object System.Data.OleDb.OleDbEnumerator).GetElements() | Where-Object { $_.SOURCES_NAME -like "Microsoft.ACE.OLEDB.*" } + } + + if ($null -eq $provider) { + $provider = (New-Object System.Data.OleDb.OleDbEnumerator).GetElements() | Where-Object { $_.SOURCES_NAME -like "Microsoft.Jet.OLEDB.*" } + } + + # If a suitable provider cannot be found (If x64 and Access hasn't been installed) + # switch to x86, because it natively supports JET + if ($null -ne $provider) { + if ($provider -is [system.array]) { + $provider = $provider[$provider.GetUpperBound(0)].SOURCES_NAME + } + else { + $provider = $provider.SOURCES_NAME + } + } + + # If a provider doesn't exist, it is necessary to switch to x86 which natively supports JET. + if ($null -eq $provider) { + # While Install-Module takes care of installing modules to x86 and x64, Import-Module doesn't. + # Because of this, the Module must be exported, written to file, and imported in the x86 shell. + $definition = (Get-Command Import-DbaCsvToSql).Definition + $function = "Function Import-DbaCsvToSql { $definition }" + Set-Content "$env:TEMP\Import-DbaCsvToSql.psm1" $function + + # Encode the SQL string, since some characters may mess up after being passed a second time. + $bytes = [System.Text.Encoding]::UTF8.GetBytes($query) + $query = [System.Convert]::ToBase64String($bytes) + + # Put switches back into proper format + $switches = @() + $options = "TableLock", "CheckConstraints", "FireTriggers", "KeepIdentity", "KeepNulls", "Default", "Truncate", "FirstRowColumns", "Safe" + foreach ($option in $options) { + $optionValue = Get-Variable $option -ValueOnly -ErrorAction SilentlyContinue + if ($optionValue -eq $true) { + $switches += "-$option" + } + } + + # Perform the actual switch, which removes any registered Import-DbaCsvToSql modules + # Then imports, and finally re-executes the command. + $csv = $csv -join ","; $switches = $switches -join " " + if ($SqlCredential.count -gt 0) { + $SqlCredentialPath = "$env:TEMP\sqlcredential.xml" + Export-CliXml -InputObject $SqlCredential $SqlCredentialPath + } + $command = "Import-DbaCsvToSql -Csv $csv -SqlInstance '$SqlInstance'-Database '$database' -Table '$table' -Delimiter '$InternalDelimiter' -First $First -Query '$query' -Batchsize $BatchSize -NotifyAfter $NotifyAfter $switches -shellswitch" + + if ($SqlCredentialPath.length -gt 0) { + $command += " -SqlCredentialPath $SqlCredentialPath" + } + Write-Verbose "Switching to x86 shell, then switching back." + &"$env:windir\syswow64\windowspowershell\v1.0\powershell.exe" "$command" + return + } + } + + # Do the first few lines contain the specified delimiter? + foreach ($file in $csv) { + try { $firstfewlines = Get-Content $file -First 3 -ErrorAction Stop } + catch { throw "$file is in use." } + if ($SingleColumn -ne $true ) { + foreach ($line in $firstfewlines) { + if (($line -match $InternalDelimiter) -eq $false) { + throw "Delimiter $InternalDelimiter not found in first row of $file." + } + } + } + } + + # If more than one csv specified, check to ensure number of columns match + if ($csv -is [system.array]) { + if ($SingleColumn -ne $true) { + $numberofcolumns = ((Get-Content $csv[0] -First 1 -ErrorAction Stop) -Split $InternalDelimiter).Count + + foreach ($file in $csv) { + $firstline = Get-Content $file -First 1 -ErrorAction Stop + $newnumcolumns = ($firstline -Split $InternalDelimiter).Count + if ($newnumcolumns -ne $numberofcolumns) { + throw "Multiple csv file mismatch. Do both use the same delimiter and have the same number of columns?" + } + } + } + } + + # Automatically generate Table name if not specified, then prompt user to confirm + if ($table.length -eq 0) { + $table = [IO.Path]::GetFileNameWithoutExtension($csv[0]) + + #Count the dots in the file name. + #1 dot, treat it as schema.table naming + #2 or more dots, really should catch it as bad practice, but the rest of the script appears to let it pass + if (($table.ToCharArray() | Where-Object {$_ -eq '.'} | Measure-Object).count -gt 0) { + if (($schema -ne $table.Split('.')[0]) -and ($schema -ne 'dbo')) { + $title = "Conflicting schema names specified" + $message = "Please confirm which schema you want to use." + $schemaA = New-Object System.Management.Automation.Host.ChoiceDescription "&A - $schema", "Use schema name $schema for import." + $schemaB = New-Object System.Management.Automation.Host.ChoiceDescription "&B - $($table.Split('.')[0])", "Use schema name $($table.Split('.')[0]) for import." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($schemaA, $schemaB) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + if ($result -eq 1) { + $schema = $table.Split('.')[0] + $tmparray = $table.split('.') + $table = $tmparray[1..$tmparray.Length] -join '.' + } + } + + } + else { + $title = "Table name not specified." + $message = "Would you like to use the automatically generated name: $table" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Uses table name $table for import." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Allows you to specify an alternative table name." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + if ($result -eq 1) { + do { + $table = Read-Host "Please enter a table name" + } + while ($table.Length -eq 0) + } + + } + } + + # If the shell has switched, decode the $query string. + if ($shellswitch -eq $true) { + $bytes = [System.Convert]::FromBase64String($Query) + $query = [System.Text.Encoding]::UTF8.GetString($bytes) + $csv = $csv -Split "," + } + + # Create columns based on first data row of first csv. + if ($SingleColumn -ne $true) { + Write-Output "[*] Calculating column names and datatypes" + $columns = Get-Columns -Csv $Csv -Delimiter $InternalDelimiter -FirstRowColumns $FirstRowColumns + if ($columns.count -gt 255 -and $safe -eq $true) { + throw "CSV must contain fewer than 256 columns." + } + } + + if ($SingleColumn -ne $true) { + $columntext = Get-ColumnText -Csv $Csv -Delimiter $InternalDelimiter + } + + # OLEDB method requires extra checks + if ($safe -eq $true) { + # Advanced SQL queries may not work (SqlBulkCopy likes a 1 to 1 mapping), so warn the user. + if ($Query -match "GROUP BY" -or $Query -match "COUNT") { + Write-Warning "Script doesn't really support the specified query. This probably won't work, but will be attempted anyway." + } + + # Check for proper SQL syntax, which for the purposes of this module must include the word "table" + if ($query.ToLower() -notmatch "\bcsv\b") { + throw "SQL statement must contain the word 'csv'. Please see this module's documentation for more details." + } + + # In order to ensure consistent results, a schema.ini file must be created. + # If a schema.ini already exists, it will be moved to TEMP temporarily. + Write-Verbose "Creating schema.ini" + $movedschemainis = Write-Schemaini -Csv $Csv -Columns $columns -Delimiter "$InternalDelimiter" -FirstRowColumns $FirstRowColumns -ColumnText $columntext + } + + # Display SQL Server Login info + if ($sqlcredential.count -gt 0) { + $username = "SQL login $($SqlCredential.UserName)" + } + else { + $username = "Windows login $(whoami)" + } + # Open Connection to SQL Server + Write-Output "[*] Logging into $SqlInstance as $username" + $sqlconn = New-Object System.Data.SqlClient.SqlConnection + if ($SqlCredential.count -eq 0) { + $sqlconn.ConnectionString = "Data Source=$SqlInstance;Integrated Security=True;Connection Timeout=3; Initial Catalog=master" + } + else { + $sqlconn.ConnectionString = "Data Source=$SqlInstance;User Id=$($SqlCredential.UserName); Password=$($SqlCredential.GetNetworkCredential().Password);Connection Timeout=3; Initial Catalog=master" + } + + try { + $sqlconn.Open() + } + catch { + throw "Could not open SQL Server connection. Is $SqlInstance online?" + } + + # Everything will be contained within 1 transaction, even creating a new table if required + # and truncating the table, if specified. + $transaction = $sqlconn.BeginTransaction() + + # Ensure database exists + $sql = "select count(*) from master.dbo.sysdatabases where name = '$database'" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $dbexists = $sqlcmd.ExecuteScalar() + if ($dbexists -eq $false) { + throw "Database does not exist on $SqlInstance" + } + Write-Output "[*] Database exists" + + $sqlconn.ChangeDatabase($database) + + # Enure Schema exists + $sql = "select count(*) from $database.sys.schemas where name='$schema'" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $schemaexists = $sqlcmd.ExecuteScalar() + + # If Schema doesn't exist create it + # Defaulting to dbo. + if ($schemaexists -eq $false) { + Write-Output "[*] Creating schema $schema" + $sql = "CREATE SCHEMA [$schema] AUTHORIZATION dbo" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + Write-Warning "Could not create $schema" + } + + } + + # Ensure table exists + $sql = "select count(*) from $database.sys.tables where name = '$table' and schema_id=schema_id('$schema')" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $tablexists = $sqlcmd.ExecuteScalar() + + # Create the table if required. Remember, this will occur within a transaction, so if the script fails, the + # new table will no longer exist. + if ($tablexists -eq $false) { + Write-Output "[*] Table does not exist" + Write-Output "[*] Creating table" + New-SqlTable -Csv $Csv -Delimiter $InternalDelimiter -Columns $columns -ColumnText $columntext -SqlConn $sqlconn -Transaction $transaction + } + else { + Write-Output "[*] Table exists" + } + + # Truncate if specified. Remember, this will occur within a transaction, so if the script fails, the + # truncate will not be committed. + if ($truncate -eq $true) { + Write-Output "[*] Truncating table" + $sql = "TRUNCATE TABLE [$schema].[$table]" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + try { + $null = $sqlcmd.ExecuteNonQuery() + } + catch { + Write-Warning "Could not truncate $schema.$table" + } + } + + # Get columns for column mapping + if ($null -eq $columnMappings) { + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]" }) + $sql = "select name from sys.columns where object_id = object_id('$schema.$table') order by column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.Load($sqlcmd.ExecuteReader()) + } + + # Time to import! + $elapsed = [System.Diagnostics.Stopwatch]::StartNew() + + # Process each CSV file specified + foreach ($file in $csv) { + + # Dynamically set NotifyAfter if it wasn't specified + if ($notifyAfter -eq 0) { + if ($resultcount -is [int]) { + $notifyafter = $resultcount / 10 + } + else { + $notifyafter = 50000 + } + } + + # Setup bulk copy + Write-Output "[*] Starting bulk copy for $(Split-Path $file -Leaf)" + + # Setup bulk copy options + $bulkCopyOptions = @() + $options = "TableLock", "CheckConstraints", "FireTriggers", "KeepIdentity", "KeepNulls", "Default", "Truncate" + foreach ($option in $options) { + $optionValue = Get-Variable $option -ValueOnly -ErrorAction SilentlyContinue + if ($optionValue -eq $true) { + $bulkCopyOptions += "$option" + } + } + $bulkCopyOptions = $bulkCopyOptions -join " & " + + # Create SqlBulkCopy using default options, or options specified in command line. + if ($bulkCopyOptions.count -gt 1) { + $bulkcopy = New-Object Data.SqlClient.SqlBulkCopy($oleconnstring, $bulkCopyOptions, $transaction) + } + else { + $bulkcopy = New-Object Data.SqlClient.SqlBulkCopy($sqlconn, "Default", $transaction) + } + + $bulkcopy.DestinationTableName = "[$schema].[$table]" + $bulkcopy.bulkcopyTimeout = 0 + $bulkCopy.BatchSize = $BatchSize + $bulkCopy.NotifyAfter = $NotifyAfter + + if ($safe -eq $true) { + # Setup bulkcopy mappings + for ($columnid = 0; $columnid -lt $sqlcolumns.rows.count; $columnid++) { + $null = $bulkCopy.ColumnMappings.Add($olecolumns[$columnid], $sqlcolumns.rows[$columnid].ItemArray[0]) + } + + # Setup the connection string. Data Source is the directory that contains the csv. + # The file name is also the table name, but with a "#" instead of a "." + $datasource = Split-Path $file + $tablename = (Split-Path $file -leaf).Replace(".", "#") + $oleconnstring = "Provider=$provider;Data Source=$datasource;Extended Properties='text';" + + # To make command line queries easier, let the user just specify "csv" instead of the + # OleDbconnection formatted name (file.csv -> file#csv) + $sql = $Query -replace "\bcsv\b", " [$tablename]" + + # Setup the OleDbconnection + $oleconn = New-Object System.Data.OleDb.OleDbconnection + $oleconn.ConnectionString = $oleconnstring + + # Setup the OleDBCommand + $olecmd = New-Object System.Data.OleDB.OleDBCommand + $olecmd.Connection = $oleconn + $olecmd.CommandText = $sql + + try { + $oleconn.Open() + } + catch { + throw "Could not open OLEDB connection." + } + + # Attempt to get the number of results so that a nice progress bar can be displayed. + # This takes extra time, and files over 100MB take too long, so just skip them. + if ($sql -match "GROUP BY") { + Write-Warning -Message "Query contains GROUP BY clause. Skipping result count." + } + else { + Write-Output "[*] Determining total rows to be copied. This may take a few seconds." + } + + if ($sql -match "\bselect top\b") { + try { + $split = $sql -split "\bselect top \b" + $resultcount = [int]($split[1].Trim().Split()[0]) + Write-Output "[*] Attempting to fetch $resultcount rows" + } + catch { + Write-Warning "Couldn't determine total rows to be copied." + } + } + elseif ($sql -notmatch "GROUP BY") { + $filesize = (Get-ChildItem $file).Length / 1MB + if ($filesize -lt 100) { + try { + $split = $sql -split "\bfrom\b" + $sqlcount = "select count(*) from $($split[1])" + # Setup the OleDBCommand + $olecmd = New-Object System.Data.OleDB.OleDBCommand + $olecmd.Connection = $oleconn + $olecmd.CommandText = $sqlcount + $resultcount = [int]($olecmd.ExecuteScalar()) + Write-Output "[*] $resultcount rows will be copied" + } + catch { + Write-Warning "Couldn't determine total rows to be copied" + } + } + else { + Write-Output "[*] File is too large for efficient result count; progress bar will not be shown." + } + } + } + + # Write to server :D + try { + if ($safe -ne $true) { + # Check to ensure batchsize isn't equal to 0 + if ($batchsize -eq 0) { + write-warning "Invalid batchsize for this operation. Increasing to 50k" + $batchsize = 50000 + } + + # Open the text file from disk + $reader = New-Object System.IO.StreamReader($file) + if ($FirstRowColumns -eq $true) { + $null = $reader.readLine() + } + + # Create the reusable datatable. Columns will be genereated using info from SQL. + $datatable = New-Object System.Data.DataTable + + # Get table column info from SQL Server + $sql = "SELECT c.name as colname, t.name as datatype, c.max_length, c.is_nullable FROM sys.columns c + JOIN sys.types t ON t.system_type_id = c.system_type_id + WHERE c.object_id = object_id('$schema.$table') and t.name != 'sysname' + order by c.column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $sqlcolumns = New-Object System.Data.DataTable + $sqlcolumns.load($sqlcmd.ExecuteReader()) + + foreach ($sqlcolumn in $sqlcolumns) { + $datacolumn = $datatable.Columns.Add() + $colname = $sqlcolumn.colname + $datacolumn.AllowDBNull = $sqlcolumn.is_nullable + $datacolumn.ColumnName = $colname + $datacolumn.DefaultValue = [DBnull]::Value + $datacolumn.Datatype = [string] + + # The following data types can sometimes cause issues when they are null + # so we will treat them differently + $convert = "bigint", "DateTimeOffset", "UniqueIdentifier", "smalldatetime", "datetime" + if ($convert -notcontains $sqlcolumn.datatype -and $turbo -ne $true) { + $null = $bulkCopy.ColumnMappings.Add($datacolumn.ColumnName, $sqlcolumn.colname) + } + } + # For the columns that cause trouble, we'll add an additional column to the datatable + # which will perform a conversion. + # Setting $column.datatype alone doesn't work as well as setting+converting. + if ($turbo -ne $true) { + $calcolumns = $sqlcolumns | Where-Object { $convert -contains $_.datatype } + foreach ($calcolumn in $calcolumns) { + $colname = $calcolumn.colname + $null = $newcolumn = $datatable.Columns.Add() + $null = $newcolumn.ColumnName = "computed$colname" + switch ($calcolumn.datatype) { + "bigint" { + $netdatatype = "System.Int64"; + $newcolumn.Datatype = [int64] + } + "DateTimeOffset" { + $netdatatype = "System.DateTimeOffset"; + $newcolumn.Datatype = [DateTimeOffset] + } + "UniqueIdentifier" { + $netdatatype = "System.Guid"; + $newcolumn.Datatype = [Guid] + } + {"smalldatetime", "datetime" -contains $_ } { + $netdatatype = "System.DateTime"; + $newcolumn.Datatype = [DateTime] + } + } + # Use a data column expression to facilitate actual conversion + $null = $newcolumn.Expression = "Convert($colname, $netdatatype)" + $null = $bulkCopy.ColumnMappings.Add($newcolumn.ColumnName, $calcolumn.colname) + } + } + + # Check to see if file has quote identified data (ie. "first","second","third") + $quoted = $false + $checkline = Get-Content $file -Last 1 + $checkcolumns = $checkline.Split($InternalDelimiter) + foreach ($checkcolumn in $checkcolumns) { + if ($checkcolumn.StartsWith('"') -and $checkcolumn.EndsWith('"')) { + $quoted = $true + } + } + + if ($quoted -eq $true) { + Write-Warning "The CSV file appears to use quoted identifiers. This may take a little longer." + # Thanks for this, Chris! http://www.schiffhauer.com/c-split-csv-values-with-a-regular-expression/ + $pattern = "((?<=`")[^`"]*(?=`"($InternalDelimiter|$)+)|(?<=$InternalDelimiter|^)[^$InternalDelimiter`"]*(?=$InternalDelimiter|$))" + } + if ($turbo -eq $true -and $first -eq 0) { + while ($null -ne ($line = $reader.ReadLine())) { + $i++ + if ($quoted -eq $true) { + $null = $datatable.Rows.Add(($line.TrimStart('"').TrimEnd('"')) -Split "`"$InternalDelimiter`"") + } + else { + $row = $datatable.Rows.Add($line.Split($InternalDelimiter)) + } + + if (($i % $batchsize) -eq 0) { + $bulkcopy.WriteToServer($datatable) + Write-Output "[*] $i rows have been inserted in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + $datatable.Clear() + } + } + } + else { + if ($turbo -eq $true -and $first -gt 0) { Write-Warning -Message "Using -First makes turbo a little slower." } + # Start import! + while ($null -ne ($line = $reader.ReadLine())) { + $i++ + try { + if ($quoted -eq $true) { + $row = $datatable.Rows.Add(($line.TrimStart('"').TrimEnd('"')) -Split $pattern) + } + else { + $row = $datatable.Rows.Add($line.Split($InternalDelimiter)) + } + } + catch { + $row = $datatable.NewRow() + try { + $tempcolumn = $line.Split($InternalDelimiter) + $colnum = 0 + foreach ($column in $tempcolumn) { + if ($column.length -ne 0) { + $row.item($colnum) = $column + } + else { + $row.item($colnum) = [DBnull]::Value + } + $colnum++ + } + $newrow = $datatable.Rows.Add($row) + } + catch { + Write-Warning "The following line ($i) is causing issues:" + Write-Output $line.Replace($InternalDelimiter, "`n") + + if ($quoted -eq $true) { + Write-Warning "The import has failed, likely because the quoted data was a little too inconsistent. Try using the -Safe parameter." + } + + Write-Verbose "Column datatypes:" + foreach ($c in $datatable.columns) { + Write-Verbose "$($c.columnname) = $($c.datatype)" + } + Write-Error $_.Exception.Message + break + } + } + + if (($i % $batchsize) -eq 0 -or $i -eq $first) { + $bulkcopy.WriteToServer($datatable) + Write-Output "[*] $i rows have been inserted in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + $datatable.Clear() + if ($i -eq $first) { + break + } + } + } + } + # Add in all the remaining rows since the last clear + if ($datatable.Rows.Count -gt 0) { + $bulkcopy.WriteToServer($datatable) + $datatable.Clear() + } + } + else { + # Add rowcount output + $bulkCopy.Add_SqlRowscopied( { + $script:totalrows = $args[1].RowsCopied + if ($resultcount -is [int]) { + $percent = [int](($script:totalrows / $resultcount) * 100) + $timetaken = [math]::Round($elapsed.Elapsed.TotalSeconds, 2) + Write-Progress -id 1 -activity "Inserting $resultcount rows" -percentcomplete $percent -status ([System.String]::Format("Progress: {0} rows ({1}%) in {2} seconds", $script:totalrows, $percent, $timetaken)) + } + else { + Write-Host "$($script:totalrows) rows copied in $([math]::Round($elapsed.Elapsed.TotalSeconds, 2)) seconds." + } + }) + + $bulkCopy.WriteToServer($olecmd.ExecuteReader("SequentialAccess")) + if ($resultcount -is [int]) { + Write-Progress -id 1 -activity "Inserting $resultcount rows" -status "Complete" -Completed + } + + } + $completed = $true + } + catch { + # If possible, give more information about common errors. + if ($resultcount -is [int]) { Write-Progress -id 1 -activity "Inserting $resultcount rows" -status "Failed" -Completed } + $errormessage = $_.Exception.Message.ToString() + $completed = $false + if ($errormessage -like "*for one or more required parameters*") { + + Write-Error -Message "Looks like your SQL syntax may be invalid. `nCheck the documentation for more information or start with a simple -Query 'select top 10 * from csv'." + Write-Error -Message "Valid CSV columns are $columns." + + } + elseif ($errormessage -match "invalid column length") { + + # Get more information about malformed CSV input + $pattern = @("\d+") + $match = [regex]::matches($errormessage, @("\d+")) + $index = [int]($match.groups[1].Value) - 1 + $sql = "select name, max_length from sys.columns where object_id = object_id('$table') and column_id = $index" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $datatable = New-Object System.Data.DataTable + $datatable.load($sqlcmd.ExecuteReader()) + $column = $datatable.name + $length = $datatable.max_length + + if ($safe -eq $true) { + Write-Warning "Column $index ($column) contains data with a length greater than $length." + Write-Warning "SqlBulkCopy makes it pretty much impossible to know which row caused the issue, but it's somewhere after row $($script:totalrows)." + } + } + elseif ($errormessage -match "does not allow DBNull" -or $errormessage -match "The given value of type") { + + if ($tablexists -eq $false) { + Write-Error "Looks like the datatype prediction didn't work out. Please create the table manually with proper datatypes then rerun the import script." + } + else { + $sql = "select name from sys.columns where object_id = object_id('$table') order by column_id" + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand($sql, $sqlconn, $transaction) + $datatable = New-Object System.Data.DataTable + $datatable.Load($sqlcmd.ExecuteReader()) + $olecolumns = ($columns | ForEach-Object { $_ -Replace "\[|\]" }) -join ', ' + Write-Warning "Datatype mismatch." + Write-Output "[*] This is sometimes caused by null handling in SqlBulkCopy, quoted data, or the first row being column names and not data (-FirstRowColumns)." + Write-Output "[*] This could also be because the data types don't match or the order of the columns within the CSV/SQL statement " + Write-Output "[*] do not line up with the order of the table within the SQL Server.`n" + Write-Output "[*] CSV order: $olecolumns`n" + Write-Output "[*] SQL order: $($datatable.rows.name -join ', ')`n" + Write-Output "[*] If this is the case, you can reorder columns by using the -Query parameter or execute the import against a view.`n" + if ($safe -eq $false) { + Write-Output "[*] You can also try running this import using the -Safe parameter, which handles quoted text well.`n" + } + Write-Error "`n$errormessage" + } + + + } + elseif ($errormessage -match "Input string was not in a correct format" -or $errormessage -match "The given ColumnName") { + Write-Warning "CSV contents may be malformed." + Write-Error $errormessage + } + else { Write-Error $errormessage } + } + } + + if ($completed -eq $true) { + # "Note: This count does not take into consideration the number of rows actually inserted when Ignore Duplicates is set to ON." + $null = $transaction.Commit() + + if ($safe -eq $false) { + Write-Output "[*] $i total rows copied" + } + else { + $total = [System.Data.SqlClient.SqlBulkCopyExtension]::RowsCopiedCount($bulkcopy) + Write-Output "[*] $total total rows copied" + } + } + else { + Write-Output "[*] Transaction rolled back." + Write-Output "[*] (Was the proper parameter specified? Is the first row the column name?)." + } + + # Script is finished. Show elapsed time. + $totaltime = [math]::Round($elapsed.Elapsed.TotalSeconds, 2) + Write-Output "[*] Total Elapsed Time for bulk insert: $totaltime seconds" + } + + End { + # Close everything just in case & ignore errors + try { + $null = $sqlconn.close(); $null = $sqlconn.Dispose(); $null = $oleconn.close; + $null = $olecmd.Dispose(); $null = $oleconn.Dispose(); $null = $bulkCopy.close(); + $null = $bulkcopy.dispose(); $null = $reader.close; $null = $reader.dispose() + } + catch { + + } + + # Delete all the temp files + if ($SqlCredentialPath.length -gt 0) { + if ((Test-Path $SqlCredentialPath) -eq $true) { + $null = cmd /c "del $SqlCredentialPath" + } + } + + if ($shellswitch -eq $false -and $safe -eq $true) { + # Delete new schema files + Write-Verbose "Removing automatically generated schema.ini." + foreach ($file in $csv) { + $directory = Split-Path $file + $null = cmd /c "del $directory\schema.ini" | Out-Null + } + + # If a shell switch occured, delete the temporary module file. + if ((Test-Path "$env:TEMP\Import-DbaCsvToSql.psm1") -eq $true) { + cmd /c "del $env:TEMP\Import-DbaCsvToSql.psm1" | Out-Null + } + + # Move original schema.ini's back if they existed + if ($movedschemainis.count -gt 0) { + foreach ($item in $movedschemainis) { + Write-Verbose "Moving $($item.keys) back to $($item.values)." + $null = cmd /c "move $($item.keys) $($item.values)" + } + } + Write-Output "[*] Finished at $(Get-Date)" + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Import-CsvToSql + } +} \ No newline at end of file diff --git a/functions/Import-DbaPfDataCollectorSetTemplate.ps1 b/functions/Import-DbaPfDataCollectorSetTemplate.ps1 new file mode 100644 index 000000000000..ebe671eea540 --- /dev/null +++ b/functions/Import-DbaPfDataCollectorSetTemplate.ps1 @@ -0,0 +1,306 @@ +function Import-DbaPfDataCollectorSetTemplate { + <# + .SYNOPSIS + Imports a new Performance Monitor Data Collector Set Template either from the dbatools repository or a file you specify. + + .DESCRIPTION + Imports a new Performance Monitor Data Collector Set Template either from the dbatools repository or a file you specify. + When importing data collector sets from the local instance, Run As Admin is required. + + Note: The included counters will be added for all SQL instances on the machine by default. + For specific instances in addition to the default, use -Instance. + + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371952 for more information + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to servers using alternative credentials. To use: + + $scred = Get-Credential, then pass $scred object to the -Credential parameter. + + .PARAMETER Path + The path to the xml file or files. + + .PARAMETER Template + From one or more of the templates from the dbatools repository. Press Tab to cycle through the available options. + + .PARAMETER RootPath + Sets the base path where the subdirectories are created. + + .PARAMETER DisplayName + Sets the display name of the data collector set. + + .PARAMETER SchedulesEnabled + If this switch is enabled, sets a value that indicates whether the schedules are enabled. + + .PARAMETER Segment + Sets a value that indicates whether PLA creates new logs if the maximum size or segment duration is reached before the data collector set is stopped. + + .PARAMETER SegmentMaxDuration + Sets the duration that the data collector set can run before it begins writing to new log files. + + .PARAMETER SegmentMaxSize + Sets the maximum size of any log file in the data collector set. + + .PARAMETER Subdirectory + Sets a base subdirectory of the root path where the next instance of the data collector set will write its logs. + + .PARAMETER SubdirectoryFormat + Sets flags that describe how to decorate the subdirectory name. PLA appends the decoration to the folder name. For example, if you specify plaMonthDayHour, PLA appends the current month, day, and hour values to the folder name. If the folder name is MyFile, the result could be MyFile110816. + + .PARAMETER SubdirectoryFormatPattern + Sets a format pattern to use when decorating the folder name. Default is 'yyyyMMdd\-NNNNNN'. + + .PARAMETER Task + Sets the name of a Task Scheduler job to start each time the data collector set stops, including between segments. + + .PARAMETER TaskRunAsSelf + If this switch is enabled, sets a value that determines whether the task runs as the data collector set user or as the user specified in the task. + + .PARAMETER TaskArguments + Sets the command-line arguments to pass to the Task Scheduler job specified in the IDataCollectorSet::Task property. + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371992 for more information. + + .PARAMETER TaskUserTextArguments + Sets the command-line arguments that are substituted for the {usertext} substitution variable in the IDataCollectorSet::TaskArguments property. + See https://msdn.microsoft.com/en-us/library/windows/desktop/aa371993 for more information. + + .PARAMETER StopOnCompletion + If this switch is enabled, sets a value that determines whether the data collector set stops when all the data collectors in the set are in a completed state. + + .PARAMETER Instance + By default, the template will be applied to all instances. If you want to set specific ones in addition to the default, supply just the instance name. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Import-DbaPfDataCollectorSetTemplate + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' + + Creates a new data collector set named 'Long Running Query' from the dbatools repository on the SQL Server sql2017. + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' -DisplayName 'New Long running query' -Confirm + + Creates a new data collector set named "New Long Running Query" using the 'Long Running Query' template. Forces a confirmation if the template exists. + + .EXAMPLE + Get-DbaPfDataCollectorSet -SqlInstance sql2017 -Session db_ola_health | Remove-DbaData collector set + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template db_ola_health | Start-DbaData collector set + + Imports a session if it exists, then recreates it using a template. + + .EXAMPLE + Get-DbaPfDataCollectorSetTemplate | Out-GridView -PassThru | Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 + + Allows you to select a Session template then import to an instance named sql2017. + + .EXAMPLE + Import-DbaPfDataCollectorSetTemplate -SqlInstance sql2017 -Template 'Long Running Query' -Instance SHAREPOINT + + Creates a new data collector set named 'Long Running Query' from the dbatools repository on the SQL Server sql2017 for both the default and the SHAREPOINT instance. + + If you'd like to remove counters for the default instance, use Remove-DbaPfDataCollectorCounter. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")] + param ( + [parameter(ValueFromPipeline)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [string]$DisplayName, + [switch]$SchedulesEnabled, + [string]$RootPath, + [switch]$Segment, + [int]$SegmentMaxDuration, + [int]$SegmentMaxSize, + [string]$Subdirectory, + [int]$SubdirectoryFormat = 3, + [string]$SubdirectoryFormatPattern = 'yyyyMMdd\-NNNNNN', + [string]$Task, + [switch]$TaskRunAsSelf, + [string]$TaskArguments, + [string]$TaskUserTextArguments, + [switch]$StopOnCompletion, + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string[]]$Template, + [string[]]$Instance, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\perfmontemplates\collectorsets.xml" + + $setscript = { + $setname = $args[0]; $templatexml = $args[1] + $collectorset = New-Object -ComObject Pla.DataCollectorSet + $collectorset.SetXml($templatexml) + $null = $collectorset.Commit($setname, $null, 0x0003) #add or modify. + $null = $collectorset.Query($setname, $Null) + } + + $instancescript = { + $services = Get-Service -DisplayName *sql* | Select-Object -ExpandProperty DisplayName + [regex]::matches($services, '(?<=\().+?(?=\))').Value | Where-Object { $PSItem -ne 'MSSQLSERVER' } | Select-Object -Unique + } + } + process { + if ((Test-Bound -ParameterName Path -Not) -and (Test-Bound -ParameterName Template -Not)) { + Stop-Function -Message "You must specify Path or Template" + } + + if (($Path.Count -gt 1 -or $Template.Count -gt 1) -and (Test-Bound -ParameterName Template)) { + Stop-Function -Message "Name cannot be specified with multiple files or templates because the Session will already exist" + } + + foreach ($computer in $ComputerName) { + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + foreach ($file in $template) { + $templatepath = "$script:PSModuleRoot\bin\perfmontemplates\collectorsets\$file.xml" + if ((Test-Path $templatepath)) { + $Path += $templatepath + } + else { + Stop-Function -Message "Invalid template ($templatepath does not exist)" -Continue + } + } + + foreach ($file in $Path) { + + if ((Test-Bound -ParameterName DisplayName -Not)) { + Set-Variable -Name DisplayName -Value (Get-ChildItem -Path $file).BaseName + } + + $Name = $DisplayNameUnresolved = $DisplayName + + Write-Message -Level Verbose -Message "Processing $file for $computer" + + if ((Test-Bound -ParameterName RootPath -Not)) { + Set-Variable -Name RootName -Value "%systemdrive%\PerfLogs\Admin\$Name" + } + + # Perform replace + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("").TrimEnd("\") + $tempfile = "$temp\import-dbatools-perftemplate.xml" + + try { + # Get content + $contents = Get-Content $file -ErrorAction Stop + + # Replace content + $replacements = 'RootPath', 'DisplayName', 'SchedulesEnabled', 'Segment', 'SegmentMaxDuration', 'SegmentMaxSize', 'SubdirectoryFormat', 'SubdirectoryFormatPattern', 'Task', 'TaskRunAsSelf', 'TaskArguments', 'TaskUserTextArguments', 'StopOnCompletion', 'DisplayNameUnresolved' + + foreach ($replacement in $replacements) { + $phrase = "<$replacement>" + $value = (Get-Variable -Name $replacement).Value + if ($value -eq $false) { + $value = "0" + } + if ($value -eq $true) { + $value = "1" + } + $replacephrase = "<$replacement>$value" + $contents = $contents.Replace($phrase, $replacephrase) + } + + # Set content + $null = Set-Content -Path $tempfile -Value $contents -Encoding Unicode + $xml = [xml](Get-Content $tempfile -ErrorAction Stop) + $plainxml = Get-Content $tempfile -ErrorAction Stop -Raw + $file = $tempfile + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + if (-not $xml.DataCollectorSet) { + Stop-Function -Message "$file is not a valid Performance Monitor template document" -Continue + } + + try { + Write-Message -Level Verbose -Message "Importing $file as $name " + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command" + + if ($instance) { + $instances = $instance + } + else { + $instances = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $instancescript -ErrorAction Stop -Raw + } + + $scriptblock = { + try { + $results = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $Name, $plainxml -ErrorAction Stop + Write-Message -Level Verbose -Message " $results" + } + catch { + Stop-Function -Message "Failure starting $setname on $computer" -ErrorRecord $_ -Target $computer -Continue + } + } + + if ((Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name)) { + if ($Pscmdlet.ShouldProcess($computer, "CollectorSet $Name already exists. Modify?")) { + Invoke-Command -Scriptblock $scriptblock + $output = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name + } + } + else { + if ($Pscmdlet.ShouldProcess($computer, "Importing collector set $Name")) { + Invoke-Command -Scriptblock $scriptblock + $output = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name + } + } + + $newcollection = @() + foreach ($instance in $instances) { + $datacollector = Get-DbaPfDataCollectorSet -ComputerName $computer -CollectorSet $Name | Get-DbaPfDataCollector + $sqlcounters = $datacollector | Get-DbaPfDataCollectorCounter | Where-Object { $_.Name -match 'sql.*\:' -and $_.Name -notmatch 'sqlclient' } | Select-Object -ExpandProperty Name + + foreach ($counter in $sqlcounters) { + $split = $counter.Split(":") + $firstpart = switch ($split[0]) { + 'SQLServer' { 'MSSQL' } + '\SQLServer' { '\MSSQL' } + default { $split[0] } + } + $secondpart = $split[-1] + $finalcounter = "$firstpart`$$instance`:$secondpart" + $newcollection += $finalcounter + } + } + + if ($newcollection.Count) { + if ($Pscmdlet.ShouldProcess($computer, "Adding $($newcollection.Count) additional counters")) { + $null = Add-DbaPfDataCollectorCounter -InputObject $datacollector -Counter $newcollection + } + } + + Remove-Item $tempfile -ErrorAction SilentlyContinue + $output + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $store -Continue + } + } + } + } +} diff --git a/functions/Import-DbaSpConfigure.ps1 b/functions/Import-DbaSpConfigure.ps1 new file mode 100644 index 000000000000..0b60f5016ce1 --- /dev/null +++ b/functions/Import-DbaSpConfigure.ps1 @@ -0,0 +1,193 @@ +function Import-DbaSpConfigure { + <# + .SYNOPSIS + Updates sp_configure settings on destination server. + + .DESCRIPTION + Updates sp_configure settings on destination server. + + .PARAMETER Source + Source SQL Server. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + Destination SQL Server. You must have sysadmin access and the server must be SQL Server 2000 or higher. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER SqlInstance + Specifies a SQL Server instance to set up sp_configure values on using a SQL file. + + .PARAMETER SqlCredential + Use this SQL credential if you are setting up sp_configure values from a SQL file. + + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the path to a SQL script file holding sp_configure queries for each of the settings to be changed. Export-DbaSPConfigure creates a suitable file as its output. + + .PARAMETER Force + If this switch is enabled, no version check between Source and Destination is performed. By default, the major and minor versions of Source and Destination must match when copying sp_configure settings. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Import-DbaSpConfigure sqlserver sqlcluster $SourceSqlCredential $DestinationSqlCredential + + Imports the sp_configure settings from the source server sqlserver and sets them on the sqlcluster server + using the SQL credentials stored in the variables + + .EXAMPLE + Import-DbaSpConfigure -SqlInstance sqlserver -Path .\spconfig.sql -SqlCredential $SqlCredential + + Imports the sp_configure settings from the file .\spconfig.sql and sets them on the sqlcluster server + using the SQL credential stored in the variables + + .OUTPUTS + $true if success + $false if failure + + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Parameter(ParameterSetName = "ServerCopy")] + [DbaInstanceParameter]$Source, + [Parameter(ParameterSetName = "ServerCopy")] + [DbaInstanceParameter]$Destination, + [Parameter(ParameterSetName = "ServerCopy")] + [PSCredential]$SourceSqlCredential, + [Parameter(ParameterSetName = "ServerCopy")] + [PSCredential]$DestinationSqlCredential, + [Parameter(ParameterSetName = "FromFile")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Parameter(ParameterSetName = "FromFile")] + [string]$Path, + [Parameter(ParameterSetName = "FromFile")] + [PSCredential]$SqlCredential, + [switch]$Force + + ) + begin { + + if ($Path.length -eq 0) { + $sourceserver = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + $destserver = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + + $source = $sourceserver.DomainInstanceName + $destination = $destserver.DomainInstanceName + } + else { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + if ((Test-Path $Path) -eq $false) { + throw "File Not Found" + } + } + + } + process { + if ($Path.length -eq 0) { + if ($Pscmdlet.ShouldProcess($destination, "Export sp_configure")) { + $sqlfilename = Export-SqlSpConfigure $sourceserver + } + + if ($sourceserver.versionMajor -ne $destserver.versionMajor -and $force -eq $false) { + Write-Warning "Source SQL Server major version and Destination SQL Server major version must match for sp_configure migration. Use -Force to override this precaution or check the exported sql file, $sqlfilename, and run manually." + return + } + + If ($Pscmdlet.ShouldProcess($destination, "Execute sp_configure")) { + $sourceserver.Configuration.ShowAdvancedOptions.ConfigValue = $true + $sourceserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + $destserver.Configuration.ShowAdvancedOptions.ConfigValue = $true + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + + $destprops = $destserver.Configuration.Properties + + foreach ($sourceprop in $sourceserver.Configuration.Properties) { + $displayname = $sourceprop.DisplayName + + $destprop = $destprops | where-object { $_.Displayname -eq $displayname } + if ($null -ne $destprop) { + try { + $destprop.configvalue = $sourceprop.configvalue + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + Write-Output "updated $($destprop.displayname) to $($sourceprop.configvalue)." + } + catch { + Write-Error "Could not $($destprop.displayname) to $($sourceprop.configvalue). Feature may not be supported." + } + } + } + try { + $destserver.Configuration.Alter() + } + catch { + $needsrestart = $true + } + + $sourceserver.Configuration.ShowAdvancedOptions.ConfigValue = $false + $sourceserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + $destserver.Configuration.ShowAdvancedOptions.ConfigValue = $false + $destserver.Query("RECONFIGURE WITH OVERRIDE") | Out-Null + + if ($needsrestart -eq $true) { + Write-Warning "Some configuration options will be updated once SQL Server is restarted." + } + else { + Write-Output "Configuration option has been updated." + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Removing temp file")) { + Remove-Item $sqlfilename -ErrorAction SilentlyContinue + } + + } + else { + if ($Pscmdlet.ShouldProcess($destination, "Importing sp_configure from $Path")) { + $server.Configuration.ShowAdvancedOptions.ConfigValue = $true + $sql = Get-Content $Path + foreach ($line in $sql) { + try { + $server.Query($line) | Out-Null + Write-Output "Successfully executed $line." + } + catch { + Write-Error "$line failed. Feature may not be supported." + } + } + $server.Configuration.ShowAdvancedOptions.ConfigValue = $false + Write-Warning "Some configuration options will be updated once SQL Server is restarted." + } + } + } + end { + if ($Path.length -gt 0) { + $server.ConnectionContext.Disconnect() + } + else { + $sourceserver.ConnectionContext.Disconnect() + $destserver.ConnectionContext.Disconnect() + } + + If ($Pscmdlet.ShouldProcess("console", "Showing finished message")) { + Write-Output "SQL Server configuration options migration finished." + } + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Import-SqlSpConfigure + } +} \ No newline at end of file diff --git a/functions/Import-DbaXESessionTemplate.ps1 b/functions/Import-DbaXESessionTemplate.ps1 new file mode 100644 index 000000000000..f516a69bb56e --- /dev/null +++ b/functions/Import-DbaXESessionTemplate.ps1 @@ -0,0 +1,211 @@ +function Import-DbaXESessionTemplate { + <# + .SYNOPSIS + Imports a new XESession XML Template + + .DESCRIPTION + Imports a new XESession XML Template either from the dbatools repository or a file you specify. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The Name of the session to create. + + .PARAMETER Path + The path to the xml file or files for the session(s). + + .PARAMETER Template + Specifies the name of one of the templates from the dbatools repository. Press tab to cycle through the provided templates. + + .PARAMETER TargetFilePath + By default, files will be created in the default xel directory. Use TargetFilePath to change all instances of + filename = "file.xel" to filename = "$TargetFilePath\file.xel". Only specify the directory, not the file itself. + + This path is relative to the destination directory + + .PARAMETER TargetFileMetadataPath + By default, files will be created in the default xem directory. Use TargetFileMetadataPath to change all instances of + filename = "file.xem" to filename = "$TargetFilePath\file.xem". Only specify the directory, not the file itself. + + This path is relative to the destination directory + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Import-DbaXESessionTemplate + + .EXAMPLE + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_query_wait_stats + + Creates a new XESession named db_query_wait_stats from the dbatools repository to the SQL Server sql2017. + + .EXAMPLE + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_query_wait_stats -Name "Query Wait Stats" + + Creates a new XESession named "Query Wait Stats" using the db_query_wait_stats template. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2017 -Session db_ola_health | Remove-DbaXESession + Import-DbaXESessionTemplate -SqlInstance sql2017 -Template db_ola_health | Start-DbaXESession + + Imports a session if it exists, then recreates it using a template. + + .EXAMPLE + Get-DbaXESessionTemplate | Out-GridView -PassThru | Import-DbaXESessionTemplate -SqlInstance sql2017 + + Allows you to select a Session template then import to an instance named sql2017. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Name, + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string[]]$Template, + [string]$TargetFilePath, + [string]$TargetFileMetadataPath, + [switch]$EnableException + ) + begin { + $metadata = Import-Clixml "$script:PSModuleRoot\bin\xetemplates-metadata.xml" + } + process { + if ((Test-Bound -ParameterName Path -Not) -and (Test-Bound -ParameterName Template -Not)) { + Stop-Function -Message "You must specify Path or Template." + } + + if (($Path.Count -gt 1 -or $Template.Count -gt 1) -and (Test-Bound -ParameterName Template)) { + Stop-Function -Message "Name cannot be specified with multiple files or templates because the Session will already exist." + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + foreach ($file in $template) { + $templatepath = "$script:PSModuleRoot\bin\xetemplates\$file.xml" + if ((Test-Path $templatepath)) { + $Path += $templatepath + } + else { + Stop-Function -Message "Invalid template ($templatepath does not exist)." -Continue + } + } + + foreach ($file in $Path) { + + if ((Test-Bound -Not -ParameterName TargetFilePath)) { + Write-Message -Level Verbose -Message "Importing $file to $instance" + try { + $xml = [xml](Get-Content $file -ErrorAction Stop) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + } + else { + Write-Message -Level Verbose -Message "TargetFilePath specified, changing all file locations in $file for $instance." + Write-Message -Level Verbose -Message "TargetFileMetadataPath specified, changing all metadata file locations in $file for $instance." + + # Handle whatever people specify + $TargetFilePath = $TargetFilePath.TrimEnd("\") + $TargetFileMetadataPath = $TargetFileMetadataPath.TrimEnd("\") + $TargetFilePath = "$TargetFilePath\" + $TargetFileMetadataPath = "$TargetFileMetadataPath\" + + # Perform replace + $xelphrase = 'name="filename" value="' + $xemphrase = 'name="metadatafile" value="' + + try { + $basename = (Get-ChildItem $file).Basename + $contents = Get-Content $file -ErrorAction Stop + $contents = $contents.Replace($xelphrase, "$xelphrase$TargetFilePath") + $contents = $contents.Replace($xemphrase, "$xemphrase$TargetFileMetadataPath") + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("").TrimEnd("\") + $tempfile = "$temp\$basename" + $null = Set-Content -Path $tempfile -Value $contents -Encoding UTF8 + $xml = [xml](Get-Content $tempfile -ErrorAction Stop) + $file = $tempfile + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + + Write-Message -Level Verbose -Message "$TargetFilePath does not exist on $server, creating now." + try { + if (-not (Test-DbaSqlPath -SqlInstance $server -Path $TargetFilePath)) { + $null = New-DbaSqlDirectory -SqlInstance $server -Path $TargetFilePath + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $file -Continue + } + } + + if (-not $xml.event_sessions) { + Stop-Function -Message "$file is not a valid XESession template document." -Continue + } + + if ((Test-Bound -ParameterName Name -not)) { + $Name = (Get-ChildItem $file).BaseName + } + + # This could be done better but not today + $no2012 = ($metadata | Where-Object Compatibility -gt 2012).Name + $no2014 = ($metadata | Where-Object Compatibility -gt 2014).Name + + if ($Name -in $no2012 -and $server.VersionMajor -eq 11) { + Stop-Function -Message "$Name is not supported in SQL Server 2012 ($server)" -Continue + } + + if ($Name -in $no2014 -and $server.VersionMajor -eq 12) { + Stop-Function -Message "$Name is not supported in SQL Server 2014 ($server)" -Continue + } + + if ((Get-DbaXESession -SqlInstance $server -Session $Name)) { + Stop-Function -Message "$Name already exists on $instance" -Continue + } + + try { + Write-Message -Level Verbose -Message "Importing $file as $name " + $session = $store.CreateSessionFromTemplate($Name, $file) + $session.Create() + if ($file -eq $tempfile) { + Remove-Item $tempfile -ErrorAction SilentlyContinue + } + Get-DbaXESession -SqlInstance $server -Session $session.Name + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $store -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Install-DbaFirstResponderKit.ps1 b/functions/Install-DbaFirstResponderKit.ps1 new file mode 100644 index 000000000000..6f2971e7bab5 --- /dev/null +++ b/functions/Install-DbaFirstResponderKit.ps1 @@ -0,0 +1,193 @@ +function Install-DbaFirstResponderKit { + <# + .SYNOPSIS + Installs or updates the First Responder Kit stored procedures. + + .DESCRIPTION + Downloads, extracts and installs the First Responder Kit stored procedures: + sp_Blitz, sp_BlitzWho, sp_BlitzFirst, sp_BlitzIndex, sp_BlitzCache and sp_BlitzTrace. + + First Responder Kit links: + http://FirstResponderKit.org + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database to instal the First Responder Kit stored procedures into + + .PARAMETER Branch + Specifies an alternate branch of the First Responder Kit to install. (master or dev) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: BrentOzar, FRK, FirstResponderKit + Author: Tara Kizer, Brent Ozar Unlimited (https://www.brentozar.com/) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Install-DbaFirstResponderKit + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1 -Database master + + Logs into server1 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1\instance1 -Database DBA + + Logs into server1\instance1 with Windows authentication and then installs the FRK in the DBA database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance server1\instance1 -Database master -SqlCredential $cred + + Logs into server1\instance1 with SQL authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance sql2016\standardrtm, sql2016\sqlexpress, sql2014 + + Logs into sql2016\standardrtm, sql2016\sqlexpress and sql2014 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + $servers = "sql2016\standardrtm", "sql2016\sqlexpress", "sql2014" + $servers | Install-DbaFirstResponderKit + + Logs into sql2016\standardrtm, sql2016\sqlexpress and sql2014 with Windows authentication and then installs the FRK in the master database. + + .EXAMPLE + Install-DbaFirstResponderKit -SqlInstance sql2016 -Branch dev + + Installs the dev branch version of the FRK in the master database on sql2016 instance. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [ValidateSet('master', 'dev')] + [string]$Branch = "master", + [object]$Database = "master", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $url = "https://codeload.github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/zip/$Branch" + + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\SQL-Server-First-Responder-Kit-$Branch.zip" + $zipfolder = "$temp\SQL-Server-First-Responder-Kit-$Branch\" + + if ($zipfile | Test-Path) { + Remove-Item -Path $zipfile -ErrorAction SilentlyContinue + } + + if ($zipfolder | Test-Path) { + Remove-Item -Path $zipfolder -Recurse -ErrorAction SilentlyContinue + } + + $null = New-Item -ItemType Directory -Path $zipfolder -ErrorAction SilentlyContinue + + Write-Message -Level Verbose -Message "Downloading and unzipping the First Responder Kit zip file." + + try { + $oldSslSettings = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + try { + Invoke-WebRequest $url -OutFile $zipfile + } + catch { + # Try with default proxy and usersettings + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + Invoke-WebRequest $url -OutFile $zipfile + } + [System.Net.ServicePointManager]::SecurityProtocol = $oldSslSettings + + # Unblock if there's a block + Unblock-File $zipfile -ErrorAction SilentlyContinue + + # Unzip the files + $shell = New-Object -ComObject Shell.Application + $zip = $shell.NameSpace($zipfile) + + foreach ($item in $zip.items()) { + $shell.Namespace($temp).CopyHere($item) + } + + Remove-Item -Path $zipfile + } + catch { + Stop-Function -Message "Couldn't download the First Responder Kit. Download and install manually from https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/archive/$Branch.zip." -ErrorRecord $_ + return + } + } + + process { + if (Test-FunctionInterrupt) { + return + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Output -Message "Starting installing/updating the First Responder Kit stored procedures in $database on $instance." + $allprocedures_query = "select name from sys.procedures where is_ms_shipped = 0" + $allprocedures = ($server.Query($allprocedures_query, $Database)).Name + # Install/Update each FRK stored procedure + foreach ($script in (Get-ChildItem $zipfolder -Filter sp_Blitz*.sql)) { + $scriptname = $script.Name + if ($scriptname -ne "sp_BlitzRS.sql") { + $sql = [IO.File]::ReadAllText($script.FullName) + + if ($scriptname -eq "sp_BlitzQueryStore.sql") { + if ($server.VersionMajor -lt 13) { continue } + } + + foreach ($query in ($sql -Split "\nGO\b")) { + $query = $query.Trim() + if ($query) { + try { + $null = $server.Query($query, $Database) + } + catch { + Write-Message -Level Warning -Message "Could not execute at least one portion of $scriptname in $Database on $instance." -ErrorRecord $_ + } + } + } + } + $baseres = @{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $Database + Name = $scriptname.TrimEnd('.sql') + } + if ($scriptname.TrimEnd('.sql') -in $allprocedures) { + $baseres['Status'] = 'Updated' + } + else { + $baseres['Status'] = 'Installed' + } + [PSCustomObject]$baseres + } + Write-Message -Level Output -Message "Finished installing/updating the First Responder Kit stored procedures in $database on $instance." + } + } +} diff --git a/functions/Install-DbaMaintenanceSolution.ps1 b/functions/Install-DbaMaintenanceSolution.ps1 new file mode 100644 index 000000000000..ce56253db010 --- /dev/null +++ b/functions/Install-DbaMaintenanceSolution.ps1 @@ -0,0 +1,328 @@ +function Install-DbaMaintenanceSolution { + <# + .SYNOPSIS + Download and Install SQL Server Maintenance Solution created by Ola Hallengren (https://ola.hallengren.com) + .DESCRIPTION + This script will download and install the latest version of SQL Server Maintenance Solution created by Ola Hallengren + + .PARAMETER SqlInstance + The target SQL Server instance onto which the Maintenance Solution will be installed. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database where Ola Hallengren's solution will be installed. Defaults to master. + + .PARAMETER BackupLocation + Location of the backup root directory. If this is not supplied, the default backup directory will be used. + + .PARAMETER CleanupTime + Time in hours, after which backup files are deleted. + + .PARAMETER OutputFileDirectory + Specify the output file directory where the Maintenance Solution will write to. + + .PARAMETER ReplaceExisting + If this switch is enabled, objects already present in the target database will be dropped and recreated. + + .PARAMETER LogToTable + If this switch is enabled, the Maintenance Solution will be configured to log commands to a table. + + .PARAMETER Solution + Specifies which portion of the Maintenance solution to install. Valid values are All (full solution), Backup, IntegrityCheck and IndexOptimize. + + .PARAMETER InstallJobs + If this switch is enabled, the corresponding SQL Agent Jobs will be created. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Ola, Maintenance + Author: Viorel Ciucu, viorel.ciucu@gmail.com, cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + http://dbatools.io/Install-DbaMaintenanceSolution + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -CleanupTime 72 + + Installs Ola Hallengren's Solution objects on RES14224 in the DBA database. + Backups will default to the default Backup Directory. + If the Maintenance Solution already exists, the script will be halted. + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -BackupLocation "Z:\SQLBackup" -CleanupTime 72 + + This will create the Ola Hallengren's Solution objects. Existing objects are not affected in any way. + + .EXAMPLE + Install-DbaMaintenanceSolution -SqlInstance RES14224 -Database DBA -BackupLocation "Z:\SQLBackup" -CleanupTime 72 -ReplaceExisting + + This will drop and then recreate the Ola Hallengren's Solution objects + The cleanup script will drop and recreate: + - TABLE [dbo].[CommandLog] + - STORED PROCEDURE [dbo].[CommandExecute] + - STORED PROCEDURE [dbo].[DatabaseBackup] + - STORED PROCEDURE [dbo].[DatabaseIntegrityCheck] + - STORED PROCEDURE [dbo].[IndexOptimize] + + The following SQL Agent jobs will be deleted: + - 'Output File Cleanup' + - 'IndexOptimize - USER_DATABASES' + - 'sp_delete_backuphistory' + - 'DatabaseBackup - USER_DATABASES - LOG' + - 'DatabaseBackup - SYSTEM_DATABASES - FULL' + - 'DatabaseBackup - USER_DATABASES - FULL' + - 'sp_purge_jobhistory' + - 'DatabaseIntegrityCheck - SYSTEM_DATABASES' + - 'CommandLog Cleanup' + - 'DatabaseIntegrityCheck - USER_DATABASES' + - 'DatabaseBackup - USER_DATABASES - DIFF' + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlServer')] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object]$Database = "master", + [string]$BackupLocation, + [int]$CleanupTime, + [string]$OutputFileDirectory, + [switch]$ReplaceExisting, + [switch]$LogToTable, + [ValidateSet('All', 'Backup', 'IntegrityCheck', 'IndexOptimize')] + [string]$Solution = 'All', + [switch]$InstallJobs, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -NonPooled + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ((Test-Bound -Parameter ReplaceExisting -Not)) { + $procs = Get-DbaSqlModule -SqlInstance $server -Database $Database | Where-Object Name -in 'CommandExecute', 'DatabaseBackup', 'DatabaseIntegrityCheck', 'IndexOptimize' + $table = Get-DbaTable -SqlInstance $server -Database $Database -Table CommandLog -IncludeSystemDBs | Where-Object Database -eq $Database + + if ($null -ne $procs -or $null -ne $table) { + Stop-Function -Message "The Maintenance Solution already exists in $Database on $instance. Use -ReplaceExisting to automatically drop and recreate." + return + } + } + + if ((Test-Bound -Parameter BackupLocation -Not)) { + $BackupLocation = (Get-DbaDefaultPath -SqlInstance $server).Backup + } + + Write-Message -Level Output -Message "Ola Hallengren's solution will be installed on database $Database." + + $db = $server.Databases[$Database] + + if ($InstallJobs -and $Solution -ne 'All') { + Stop-Function -Message "To create SQL Agent jobs you need to use '-Solution All' and '-InstallJobs Create'." + return + } + + if ($ReplaceExisting -eq $true) { + Write-Message -Level Verbose -Message "If Ola Hallengren's scripts are found, we will drop and recreate them!" + } + + if ($CleanupTime -ne 0 -and $InstallJobs -eq $false) { + Write-Message -Level Output -Message "CleanupTime $CleanupTime value will be ignored because you chose not to create SQL Agent Jobs." + } + + # Required + $required = @('CommandExecute.sql') + + if ($LogToTable) { + $required += 'CommandLog.sql' + } + + if ($Solution -match 'Backup') { + $required += 'DatabaseBackup.sql' + } + + if ($Solution -match 'IntegrityCheck') { + $required += 'DatabaseIntegrityCheck.sql' + } + + if ($Solution -match 'IndexOptimize') { + $required += 'IndexOptimize.sql' + } + + if ($Solution -match 'All') { + $required += 'MaintenanceSolution.sql' + } + + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\ola.zip" + + # Start the download + $url = "https://github.com/olahallengren/sql-server-maintenance-solution/archive/master.zip" + try { + Start-BitsTransfer -Source $url -DisplayName 'Downloading SQL Server Maintenance Solution - https://ola.hallengren.com' -Destination $zipfile -ErrorAction Stop + } + catch { + Stop-Function -Message "You need to re-run the script, there is a problem with the proxy or the download link has changed." -ErrorRecord $_ + } + + # Unblock if there's a block + Unblock-File $zipfile -ErrorAction SilentlyContinue + + $path = "$temp\sql-server-maintenance-solution-master" + + # We don't like default parameters messed with so we start clean + if ((Test-Path $path)) { + Remove-Item -Path $temp\sql-server-maintenance-solution-master -Recurse -Force -ErrorAction SilentlyContinue + } + + # internal if it doesn't exist + Expand-Archive -Path $zipfile -DestinationPath $temp -Force + Remove-Item -Path $zipfile + + $listOfFiles = Get-ChildItem -Filter "*.sql" -Path $path | Select-Object -ExpandProperty FullName + + # In which database we install + if ($Database -ne 'master') { + $findDB = 'USE [master]' + $replaceDB = 'USE [' + $Database + ']' + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findDB, $replaceDB) | Set-Content -Path $file + } + } + + # Backup location + if ($BackupLocation) { + $findBKP = 'C:\Backup' + $replaceBKP = $BackupLocation + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findBKP, $replaceBKP) | Set-Content -Path $file + } + } + + # CleanupTime + if ($CleanupTime -ne 0) { + $findCleanupTime = 'SET @CleanupTime = NULL' + $replaceCleanupTime = 'SET @CleanupTime = ' + $CleanupTime + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findCleanupTime, $replaceCleanupTime) | Set-Content -Path $file + } + } + + # OutputFileDirectory + if ($OutputFileDirectory.Length -gt 0) { + $findOutputFileDirectory = 'SET @OutputFileDirectory = NULL' + $replaceOutputFileDirectory = 'SET @OutputFileDirectory = N''' + $OutputFileDirectory + '''' + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findOutputFileDirectory, $replaceOutputFileDirectory) | Set-Content -Path $file + } + + } + + # LogToTable + if (!$LogToTable) { + $findLogToTable = "SET @LogToTable = 'Y'" + $replaceLogToTable = "SET @LogToTable = 'N'" + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findLogToTable, $replaceLogToTable) | Set-Content -Path $file + } + } + + # Create Jobs + if ($InstallJobs -eq $false) { + $findCreateJobs = "SET @CreateJobs = 'Y'" + $replaceCreateJobs = "SET @CreateJobs = 'N'" + foreach ($file in $listOfFiles) { + (Get-Content -Path $file -Raw).Replace($findCreateJobs, $replaceCreateJobs) | Set-Content -Path $file + } + } + + $CleanupQuery = $null + if ($ReplaceExisting) { + [string]$CleanupQuery = $(" + IF OBJECT_ID('[dbo].[CommandLog]', 'U') IS NOT NULL + DROP TABLE [dbo].[CommandLog]; + IF OBJECT_ID('[dbo].[CommandExecute]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[CommandExecute]; + IF OBJECT_ID('[dbo].[DatabaseBackup]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[DatabaseBackup]; + IF OBJECT_ID('[dbo].[DatabaseIntegrityCheck]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[DatabaseIntegrityCheck]; + IF OBJECT_ID('[dbo].[IndexOptimize]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[IndexOptimize]; + ") + + Write-Message -Level Output -Message "Dropping objects created by Ola's Maintenance Solution" + $null = $db.Query($CleanupQuery) + + # Remove Ola's Jobs + if ($InstallJobs -and $ReplaceExisting) { + Write-Message -Level Output -Message "Removing existing SQL Agent Jobs created by Ola's Maintenance Solution." + $jobs = Get-DbaAgentJob -SqlInstance $server | Where-Object Description -match "hallengren" + if ($jobs) { + $jobs | ForEach-Object { Remove-DbaAgentJob -SqlInstance $instance -Job $_.name } + } + } + } + + try { + Write-Message -Level Output -Message "Installing on server $SqlInstance, database $Database." + + foreach ($file in $listOfFiles) { + $shortFileName = Split-Path $file -Leaf + if ($required.Contains($shortFileName)) { + Write-Message -Level Output -Message "Installing $file." + $sql = [IO.File]::ReadAllText($file) + try { + foreach ($query in ($sql -Split "\nGO\b")) { + $null = $db.Query($query) + } + } + catch { + Stop-Function -Message "Could not execute $file in $Database on $instance." -ErrorRecord $_ -Target $db -Continue + } + } + } + } + catch { + Stop-Function -Message "Could not execute $file in $Database on $instance." -ErrorRecord $_ -Target $db -Continue + } + } + + if ((Test-Path $path)) { + Remove-Item -Path $temp\sql-server-maintenance-solution-master -Recurse -Force -ErrorAction SilentlyContinue + } + + # Only here due to need for non-pooled connection in this command + try { + $server.ConnectionContext.Disconnect() + } + catch { + } + + Write-Message -Level Output -Message "Installation complete." + } +} diff --git a/functions/Install-DbaWatchUpdate.ps1 b/functions/Install-DbaWatchUpdate.ps1 index b623d3e9a638..281015e7f56a 100644 --- a/functions/Install-DbaWatchUpdate.ps1 +++ b/functions/Install-DbaWatchUpdate.ps1 @@ -1,92 +1,103 @@ -Function Install-DbaWatchUpdate -{ -<# -.SYNOPSIS -Adds the scheduled task to support Watch-DbaUpdate +function Install-DbaWatchUpdate { + <# + .SYNOPSIS + Adds the scheduled task to support Watch-DbaUpdate. -.DESCRIPTION -Adds the scheduled task to support Watch-DbaUpdate. + .DESCRIPTION + Adds the scheduled task to support Watch-DbaUpdate. -.NOTES -Tags: JustForFun -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire + .PARAMETER TaskName + Provide custom name for the Scheduled Task -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. -You should have received a copy of the GNU General Public License along with this program. If not, see . + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -.LINK -https://dbatools.io/Install-DbaWatchUpdate + .NOTES + Tags: Module + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT -.EXAMPLE -Install-DbaWatchUpdate + .LINK + https://dbatools.io/Install-DbaWatchUpdate -Adds the scheduled task needed by Watch-DbaUpdate -#> - PROCESS - { - if (([Environment]::OSVersion).Version.Major -lt 10) - { - Write-Warning "This command only supports Windows 10 and above" - return - } - - $script = { - try - { - # create a task, check every 3 hours - $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -NoLogo -NonInteractive -WindowStyle Hidden Watch-DbaUpdate' - $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Hours 1) - $principal = New-ScheduledTaskPrincipal -LogonType S4U -UserId (whoami) - $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([timespan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable -DontStopOnIdleEnd - $task = Register-ScheduledTask -Principal $principal -TaskName 'dbatools version check' -Action $action -Trigger $trigger -Settings $settings -ErrorAction Stop - } - catch - { - # keep movin - } - } - - if ($null -eq (Get-ScheduledTask -TaskName "dbatools version check" -ErrorAction SilentlyContinue)) - { - # Needs admin creds to setup the kind of PowerShell window that doesn't appear for a millisecond - # which is a millisecond too long - If (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) - { - Write-Warning "Watch-DbaUpdate runs as a Scheduled Task which must be created. This will only happen once." - Start-Process powershell -Verb runAs -ArgumentList Install-DbaWatchUpdate -Wait - } - - try - { - Invoke-Command -ScriptBlock $script -ErrorAction Stop - - if ((Get-Location).Path -ne "$env:windir\system32") - { - $module = Get-Module -Name dbatools - Write-Warning "Task created! A notication should appear momentarily. Here's something cute to look at in the interim." - Show-Notification -title "dbatools wants you" -text "come hang out at dbatools.io/slack" - } - } - catch - { - Write-Warning "Couldn't create scheduled task :(" - return - } - - # doublecheck - if ($null -eq (Get-ScheduledTask -TaskName "dbatools version check" -ErrorAction SilentlyContinue)) - { - Write-Warning "Couldn't create scheduled task :(" - } - } - else - { - Write-Output "Watch-DbaUpdate is already installed :)" - Write-Output "And by already isntalled, we mean it's a scheduled task called 'dbatools version check'" - } - } -} + .EXAMPLE + Install-DbaWatchUpdate + + Adds the scheduled task needed by Watch-DbaUpdate + + .EXAMPLE + Install-DbaWatchUpdate -TaskName MyScheduledTask + + Will create the scheduled task as the name MyScheduledTask + #> + [cmdletbinding(SupportsShouldProcess)] + param( + [string]$TaskName = 'dbatools version check', + [switch]$EnableException + ) + process { + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Validate Version of OS") ) { + if (([Environment]::OSVersion).Version.Major -lt 10) { + Stop-Function -Message "This command only supports Windows 10 and above" + } + } + $script = { + try { + # create a task, check every 3 hours + $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -NoLogo -NonInteractive -WindowStyle Hidden Watch-DbaUpdate' + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Hours 1) + $principal = New-ScheduledTaskPrincipal -LogonType S4U -UserId (whoami) + $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([timespan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable -DontStopOnIdleEnd + $task = Register-ScheduledTask -Principal $principal -TaskName 'dbatools version check' -Action $action -Trigger $trigger -Settings $settings -ErrorAction Stop + } + catch { + # keep moving + } + } + + if ($null -eq (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue)) { + # Needs admin creds to setup the kind of PowerShell window that doesn't appear for a millisecond + # which is a millisecond too long + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Validate running in RunAs mode")) { + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Message -Level Warning -Message "This command has to run using RunAs mode (privileged) to create the Scheduled Task. This will only happen once." + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Starting process in RunAs mode") ) { + Start-Process powershell -Verb runAs -ArgumentList Install-DbaWatchUpdate -Wait + } + } + + } + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Creating scheduled task $TaskName")) { + try { + Invoke-Command -ScriptBlock $script -ErrorAction Stop + + if ((Get-Location).Path -ne "$env:WINDIR\system32") { + Write-Message -Level Output -Message "Scheduled Task [$TaskName] created! A notification should appear momentarily. Here's something cute to look at in the interim." + Show-Notification -Title "dbatools wants you" -Text "come hang out at dbatools.io/slack" + } + } + catch { + Stop-Function -Message "Could not create scheduled task $TaskName" -Target $env:COMPUTERNAME -ErrorRecord $_ + } + } + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Checking scheduled task was created")) { + # double check + if ($null -eq (Get-ScheduledTask -TaskName "dbatools version check" -ErrorAction SilentlyContinue)) { + Write-Message -Level Warning -Message "Scheduled Task was not created." + } + } + } + else { + Write-Message -Level Output -Message "Scheduled Task $TaskName is already installed on this machine." + } + } +} \ No newline at end of file diff --git a/functions/Install-DbaWhoIsActive.ps1 b/functions/Install-DbaWhoIsActive.ps1 new file mode 100644 index 000000000000..9bdcdee08179 --- /dev/null +++ b/functions/Install-DbaWhoIsActive.ps1 @@ -0,0 +1,247 @@ +function Install-DbaWhoIsActive { + <# + .SYNOPSIS + Automatically installs or updates sp_WhoisActive by Adam Machanic. + + .DESCRIPTION + This command downloads, extracts and installs sp_WhoisActive with Adam's permission. To read more about sp_WhoisActive, please visit http://whoisactive.com and http://sqlblog.com/blogs/adam_machanic/archive/tags/who+is+active/default.aspx + + Please consider donating to Adam if you find this stored procedure helpful: http://tinyurl.com/WhoIsActiveDonate + + Note that you will be prompted a bunch of times to confirm an action. + + .PARAMETER SqlInstance + The SQL Server instance. Server version must be SQL Server version 2005 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to install sp_WhoisActive into. This parameter is mandatory when executing this command unattended. + + .PARAMETER LocalFile + Specifies the path to a local file to install sp_WhoisActive from. This can be either the zipfile as distributed by the website or the expanded SQL script. If this parameter is not specified, the latest version will be downloaded and installed from https://whoisactive.com/ + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + If this switch is enabled, the sp_WhoisActive will be downloaded from the internet even if previously cached. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -Database master + + Downloads sp_WhoisActive from the internet and installs to sqlserver2014a's master database. Connects to SQL Server using Windows Authentication. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -SqlCredential $cred + + Pops up a dialog box asking which database on sqlserver2014a you want to install the procedure into. Connects to SQL Server using SQL Authentication. + + .EXAMPLE + Install-DbaWhoIsActive -SqlInstance sqlserver2014a -Database master -LocalFile c:\SQLAdmin\whoisactive_install.sql + + Installs sp_WhoisActive to sqlserver2014a's master database from the local file whoisactive_install.sql + + .EXAMPLE + $instances = Get-DbaRegisteredServer sqlserver + Install-DbaWhoIsActive -SqlInstance $instances -Database master + + .NOTES + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Install-DbaWhoIsActive + #> + + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $false)] + [ValidateScript( { Test-Path -Path $_ -PathType Leaf })] + [string]$LocalFile, + [object]$Database, + [switch][Alias('Silent')] + $EnableException, + [switch]$Force + ) + + begin { + $DbatoolsData = Get-DbaConfigValue -FullName "Path.DbatoolsData" + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $zipfile = "$temp\spwhoisactive.zip" + + if ($LocalFile -eq $null -or $LocalFile.Length -eq 0) { + $baseUrl = "http://whoisactive.com/downloads" + $latest = ((Invoke-WebRequest -UseBasicParsing -uri http://whoisactive.com/downloads).Links | where-object { $PSItem.href -match "who_is_active" } | Select-Object href -First 1).href + $LocalCachedCopy = Join-Path -Path $DbatoolsData -ChildPath $latest; + + if ((Test-Path -Path $LocalCachedCopy -PathType Leaf) -and (-not $Force)) { + Write-Message -Level Verbose -Message "Locally-cached copy exists, skipping download." + if ($PSCmdlet.ShouldProcess($env:computername, "Copying sp_WhoisActive from local cache for installation")) { + Copy-Item -Path $LocalCachedCopy -Destination $zipfile; + } + } + else { + if ($PSCmdlet.ShouldProcess($env:computername, "Downloading sp_WhoisActive")) { + try { + Write-Message -Level Verbose -Message "Downloading sp_WhoisActive zip file, unzipping and installing." + $url = $baseUrl + "/" + $latest + try { + Invoke-WebRequest $url -OutFile $zipfile -ErrorAction Stop -UseBasicParsing + Copy-Item -Path $zipfile -Destination $LocalCachedCopy + } + catch { + #try with default proxy and usersettings + (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + Invoke-WebRequest $url -OutFile $zipfile -ErrorAction Stop -UseBasicParsing + } + } + catch { + Stop-Function -Message "Couldn't download sp_WhoisActive. Please download and install manually from $url." -ErrorRecord $_ + return + } + } + } + } + else { + # Look local + if ($PSCmdlet.ShouldProcess($env:computername, "Copying local file to temp directory")) { + + if ($LocalFile.EndsWith("zip")) { + Copy-Item -Path $LocalFile -Destination $zipfile -Force + } + else { + Copy-Item -Path $LocalFile -Destination (Join-Path -path $temp -childpath "whoisactivelocal.sql") + } + } + } + if ($LocalFile -eq $null -or $LocalFile.Length -eq 0 -or $LocalFile.EndsWith("zip")) { + # Unpack + # Unblock if there's a block + if ($PSCmdlet.ShouldProcess($env:computername, "Unpacking zipfile")) { + + Unblock-File $zipfile -ErrorAction SilentlyContinue + + if (Get-Command -ErrorAction SilentlyContinue -Name "Expand-Archive") { + try { + Expand-Archive -Path $zipfile -DestinationPath $temp -Force + } + catch { + Stop-Function -Message "Unable to extract $zipfile. Archive may not be valid." -ErrorRecord $_ + return + } + } + else { + # Keep it backwards compatible + $shell = New-Object -ComObject Shell.Application + $zipPackage = $shell.NameSpace($zipfile) + $destinationFolder = $shell.NameSpace($temp) + Get-ChildItem "$temp\who*active*.sql" | Remove-Item + $destinationFolder.CopyHere($zipPackage.Items()) + } + Remove-Item -Path $zipfile + } + $sqlfile = (Get-ChildItem "$temp\who*active*.sql" -ErrorAction SilentlyContinue | Select-Object -First 1).FullName + } + else { + $sqlfile = $LocalFile + } + + if ($PSCmdlet.ShouldProcess($env:computername, "Reading SQL file into memory")) { + Write-Message -Level Verbose -Message "Using $sqlfile." + + $sql = [IO.File]::ReadAllText($sqlfile) + $sql = $sql -replace 'USE master', '' + $batches = $sql -split "GO\r\n" + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (-not $Database) { + if ($PSCmdlet.ShouldProcess($instance, "Prompting with GUI list of databases")) { + $Database = Show-DbaDatabaseList -SqlInstance $server -Title "Install sp_WhoisActive" -Header "To deploy sp_WhoisActive, select a database or hit cancel to quit." -DefaultDb "master" + + if (-not $Database) { + Stop-Function -Message "You must select a database to install the procedure." -Target $Database + return + } + + if ($Database -ne 'master') { + Write-Message -Level Warning -Message "You have selected a database other than master. When you run Invoke-DbaWhoIsActive in the future, you must specify -Database $Database." + } + } + } + if ($PSCmdlet.ShouldProcess($instance, "Installing sp_WhoisActive")) { + try { + $ProcedureExists_Query = "select COUNT(*) [proc_count] from sys.procedures where is_ms_shipped = 0 and name like '%sp_WhoisActive%'" + + if ($server.Databases[$Database]) { + $ProcedureExists = ($server.Query($ProcedureExists_Query, $Database)).proc_count + foreach ($batch in $batches) { + try { + $null = $server.databases[$Database].ExecuteNonQuery($batch) + } + catch { + Stop-Function -Message "Failed to install stored procedure." -ErrorRecord $_ -Continue -Target $instance + } + } + + if ($ProcedureExists -gt 0) { + $status = 'Updated' + } + else { + $status = 'Installed' + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $Database + Name = 'sp_WhoisActive' + Status = $status + } + } + else { + Stop-Function -Message "Failed to find database $Database on $instance or $Database is not writeable." -ErrorRecord $_ -Continue -Target $instance + } + + } + catch { + Stop-Function -Message "Failed to install stored procedure." -ErrorRecord $_ -Continue -Target $instance + } + + } + } + } + end { + if ($PSCmdlet.ShouldProcess($env:computername, "Post-install cleanup")) { + Get-Item $sqlfile | Remove-Item + } + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Install-SqlWhoIsActive + } +} diff --git a/functions/Install-SqlWhoIsActive.ps1 b/functions/Install-SqlWhoIsActive.ps1 deleted file mode 100644 index c71655747cc2..000000000000 --- a/functions/Install-SqlWhoIsActive.ps1 +++ /dev/null @@ -1,193 +0,0 @@ -Function Install-SqlWhoIsActive -{ -<# -.SYNOPSIS -Automatically installs or updates sp_WhoIsActive by Adam Machanic. - -.DESCRIPTION -This command downloads, extracts and installs sp_whoisactive with Adam's permission. To read more about sp_WhoIsActive, please visit: - -Updates: http://sqlblog.com/blogs/adam_machanic/archive/tags/who+is+active/default.aspx - -Also, consider donating to Adam if you find this stored procedure helpful: http://tinyurl.com/WhoIsActiveDonate - -.PARAMETER SqlServer -The SQL Server instance.You must have sysadmin access and server version must be SQL Server version 2000 or higher. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. - -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER OutputDatabaseName -Outputs just the database name instead of the success message - -.NOTES -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Install-SqlWhoIsActive - -.EXAMPLE -Install-SqlWhoIsActive -SqlServer sqlserver2014a -Database master - -Installs sp_WhoIsActive to sqlserver2014a's master database. Logs in using Windows Authentication. - -.EXAMPLE -Install-SqlWhoIsActive -SqlServer sqlserver2014a -SqlCredential $cred - -Pops up a dialog box asking which database on sqlserver2014a you want to install the proc to. Logs into SQL Server using SQL Authentication. - - -.EXAMPLE -$servers = Get-SqlRegisteredServerName sqlserver -Install-SqlWhoIsActive -SqlServer $servers -Database master - -This command doesn't support passing both servers and default database, but you can accomplish the same thing by passing an array and specifying a database. - -#> - - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [object]$SqlCredential, - [switch]$OutputDatabaseName - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabase -SqlServer $sqlserver[0] -SqlCredential $SqlCredential } } - - BEGIN - { - Function Get-SpWhoIsActive - { - - $url = 'http://whoisactive.com/who_is_active_v11_17.zip' - $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") - $zipfile = "$temp\spwhoisactive.zip" - - try - { - Invoke-WebRequest $url -OutFile $zipfile - } - catch - { - #try with default proxy and usersettings - (New-Object System.Net.WebClient).Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials - Invoke-WebRequest $url -OutFile $zipfile - } - - # Unblock if there's a block - Unblock-File $zipfile -ErrorAction SilentlyContinue - - # Keep it backwards compatible - $shell = New-Object -COM Shell.Application - $zipPackage = $shell.NameSpace($zipfile) - $destinationFolder = $shell.NameSpace($temp) - $destinationFolder.CopyHere($zipPackage.Items()) - - Remove-Item -Path $zipfile - } - } - - PROCESS - { - # Used a dynamic parameter? Convert from RuntimeDefinedParameter object to regular array - $Database = $psboundparameters.Database - - foreach ($server in $sqlserver) - { - # please continue to use these variable names for consistency - $sourceserver = Connect-SqlServer -SqlServer $server -SqlCredential $SqlCredential - $source = $sourceserver.DomainInstanceName - - if ($database.length -eq 0) - { - $database = Show-SqlDatabaseList -SqlServer $sourceserver -Title "Install sp_WhoisActive" -Header "sp_WhoIsActive not found. To deploy, select a database or hit cancel to quit." -DefaultDb "master" - - if ($database.length -eq 0) - { - throw "You must select a database to install the procedure" - } - - if ($database -ne 'master') - { - Write-Warning "You have selected a database other than master. When you run Show-SqlWhoIsActive in the future, you must specify -Database $database" - } - } - - $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") - $sqlfile = (Get-ChildItem "$temp\who*active*.sql" | Select-Object -First 1).FullName - - if ($sqlfile.Length -eq 0) - { - try - { - if ($OutputDatabaseName -eq $false) - { - Write-Output "Downloading sp_WhoIsActive zip file, unzipping and installing." - } - Get-SpWhoIsActive - } - catch - { - throw "Couldn't download sp_WhoIsActive. Please download and install manually from http://whoisactive.com/who_is_active_v11_17.zip." - } - } - - $sqlfile = (Get-ChildItem "$temp\who*active*.sql" | Select-Object -First 1).Name - $sqlfile = "$temp\$sqlfile" - - $sql = [IO.File]::ReadAllText($sqlfile) - $sql = $sql -replace 'USE master', '' - $batches = $sql -split "GO\r\n" - - foreach ($batch in $batches) - { - try - { - $null = $sourceserver.databases[$database].ExecuteNonQuery($batch) - - } - catch - { - Write-Exception $_ - throw "Can't install stored procedure. See exception text for details." - } - } - - if ($OutputDatabaseName -eq $true) - { - return $database - } - else - { - Write-Output "Finished installing/updating sp_WhoIsActive in $database on $server" - } - - $sourceserver.ConnectionContext.Disconnect() - } - } - - END - { - # nothin - } -} diff --git a/functions/Invoke-DbaAdvancedRestore.ps1 b/functions/Invoke-DbaAdvancedRestore.ps1 new file mode 100644 index 000000000000..b61756d526cb --- /dev/null +++ b/functions/Invoke-DbaAdvancedRestore.ps1 @@ -0,0 +1,358 @@ +function Invoke-DbaAdvancedRestore { + <# + .SYNOPSIS + Allows the restore of modified BackupHistory Objects + For 90% of users Restore-DbaDatabase should be your point of access to this function. The other 10% use it at their own risk + + .DESCRIPTION + This is the final piece in the Restore-DbaDatabase Stack. Usually a BackupHistory object will arrive here from Restore-DbaDatabse via the following pipeline: + Get-DbaBackupInformation | Select-DbaBackupInformation | Format-DbaBackupInformation | Test-DbaBackupInformation | Invoke-DbaAdvancedRestore + + We have exposed these functions publicly to allow advanced users to perform operations that we don't support, or won't add as they would make things too complex for the majority of our users + + For example if you wanted to do some very complex redirection during a migration, then doing the rewrite of destinations may be better done with your own custom scripts rather than via Format-DbaBackupInformation + + We would recommend ALWAYS pushing your input through Test-DbaBackupInformation just to make sure that it makes sense to us. + + .PARAMETER BackupHistory + The BackupHistory object to be restored. + Can be passed in on the pipeline + + .PARAMETER SqlInstance + The SqlInstance to which the backups should be restored + + .PARAMETER SqlCredential + SqlCredential to be used to connect to the target SqlInstance + + .PARAMETER OutputScriptOnly + If set, the restore will not be performed, but the T-SQL scripts to perform it will be returned + + .PARAMETER VerifyOnly + If set, performs a Verify of the backups rather than a full restore + + .PARAMETER RestoreTime + Point in Time to which the database should be restored. + + This should be the same value or earlier, as used in the previous pipeline stages + + .PARAMETER StandbyDirectory + A folder path where a standby file should be created to put the recovered databases in a standby mode + + .PARAMETER NoRecovery + Leave the database in a restoring state so that further restore may be made + + .PARAMETER MaxTransferSize + Parameter to set the unit of transfer. Values must be a multiple by 64kb + + .PARAMETER Blocksize + Specifies the block size to use. Must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb or 64kb + Can be specified in bytes + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER BufferCount + Number of I/O buffers to use to perform the operation. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail + + .PARAMETER Continue + Indicates that the restore is continuing a restore, so target database must be in Recovering or Standby states + + .PARAMETER AzureCredential + AzureCredential required to connect to blob storage holding the backups + + .PARAMETER WithReplace + Indicated that if the database already exists it should be replaced + + .PARAMETER KeepCDC + Indicates whether CDC information should be restored as part of the database + + .PARAMETER PageRestore + The output from Get-DbaSuspect page containing the suspect pages to be restored. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER EnableException + Replaces user friendly yellow warnings with bloody red exceptions of doom! + Use this if you want the function to throw terminating errors you want to catch. + + .NOTES + Tags: Restore, Backup + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaAdvancedRestore + + .EXAMPLE + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance + + Will restore all the backups in the BackupHistory object according to the transformations it contains + + .EXAMPLE + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance -OutputScriptOnly + $BackupHistory | Invoke-DbaAdvancedRestore -SqlInstance MyInstance + + First generates just the T-SQL restore scripts so they can be sanity checked, and then if they are good perform the full restore. By reusing the BackupHistory object there is no need to rescan all the backup files again + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object[]]$BackupHistory, + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [switch]$OutputScriptOnly, + [switch]$VerifyOnly, + [datetime]$RestoreTime = (Get-Date).AddDays(2), + [string]$StandbyDirectory, + [switch]$NoRecovery, + [int]$MaxTransferSize, + [int]$BlockSize, + [int]$BufferCount, + [switch]$Continue, + [PSCredential]$AzureCredential, + [switch]$WithReplace, + [switch]$KeepCDC, + [object[]]$PageRestore, + [switch]$EnableException + ) + begin { + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + if ($KeepCDC -and ($NoRecovery -or ('' -ne $StandbyDirectory))) { + Stop-Function -Category InvalidArgument -Message "KeepCDC cannot be specified with Norecovery or Standby as it needs recovery to work" + return + } + + if ($null -ne $PageRestore) { + Write-Message -Message "Doing Page Recovery" -Level Verbose + $tmpPages = @() + foreach ($Page in $PageRestore) { + $tmppages += "$($Page.FileId):$($Page.PageID)" + } + $NoRecovery = $True + $Pages = $tmpPages -join ',' + } + #$OutputScriptOnly = $false + $InternalHistory = @() + } + process { + foreach ($bh in $BackupHistory) { + $InternalHistory += $bh + } + } + end { + if (Test-FunctionInterrupt) { return } + $Databases = $InternalHistory.Database | Select-Object -Unique + foreach ($Database in $Databases) { + if ($Database -in $Server.Databases.Name) { + if (-not $OutputScriptOnly -and -not $VerifyOnly) { + if ($Pscmdlet.ShouldProcess("Killing processes in $Database on $SqlInstance as it exists and WithReplace specified `n", "Cannot proceed if processes exist, ", "Database Exists and WithReplace specified, need to kill processes to restore")) { + try { + Write-Message -Level Verbose -Message "Killing processes on $Database" + $null = Stop-DbaProcess -SqlInstance $Server -Database $Database -WarningAction Silentlycontinue + $null = $server.Query("Alter database $Database set offline with rollback immediate; alter database $Database set restricted_user; Alter database $Database set online with rollback immediate", 'master') + $server.ConnectionContext.Connect() + } + catch { + Write-Message -Level Verbose -Message "No processes to kill in $Database" + } + } + } + elseif (-not $WithReplace -and (-not $VerifyOnly)) { + Stop-Function -Message "$Database exists and WithReplace not specified, stopping" -EnableException $EnableException + return + } + } + Write-Message -Message "WithReplace = $WithReplace" -Level Debug + $backups = @($InternalHistory | Where-Object {$_.Database -eq $Database} | Sort-Object -Property Type, FirstLsn) + $BackupCnt = 1 + foreach ($backup in $backups) { + $Restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + if (($backup -ne $backups[-1]) -or $true -eq $NoRecovery) { + $Restore.NoRecovery = $True + } + elseif ($backup -eq $backups[-1] -and '' -ne $StandbyDirectory) { + $Restore.StandbyFile = $StandByDirectory + "\" + $Database + (get-date -Format yyyMMddHHmmss) + ".bak" + Write-Message -Level Verbose -Message "Setting standby on last file $($Restore.StandbyFile)" + } + else { + $Restore.NoRecovery = $False + } + if ($restoretime -gt (Get-Date) -or $Restore.RestoreTime -gt (Get-Date) -or $backup.RecoveryModel -eq 'Simple') { + $Restore.ToPointInTime = $null + } + else { + if ($RestoreTime -ne $Restore.RestoreTime) { + $Restore.ToPointInTime = $backup.RestoreTime + } + else { + $Restore.ToPointInTime = $RestoreTime + } + } + $Restore.Database = $database + $Restore.ReplaceDatabase = $WithReplace + if ($MaxTransferSize) { + $Restore.MaxTransferSize = $MaxTransferSize + } + if ($BufferCount) { + $Restore.BufferCount = $BufferCount + } + if ($BlockSize) { + $Restore.Blocksize = $BlockSize + } + if ($true -ne $Continue -and ($null -eq $Pages)) { + foreach ($file in $backup.FileList) { + $MoveFile = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile + $MoveFile.LogicalFileName = $File.LogicalName + $MoveFile.PhysicalFileName = $File.PhysicalName + $null = $Restore.RelocateFiles.Add($MoveFile) + } + } + $Action = switch ($backup.Type) { + '1' {'Database'} + '2' {'Log'} + '5' {'Database'} + 'Transaction Log' {'Log'} + Default {'Database'} + } + + Write-Message -Level Debug -Message "restore action = $Action" + $Restore.Action = $Action + foreach ($File in $backup.FullName) { + Write-Message -Message "Adding device $file" -Level Debug + $Device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $Device.Name = $file + if ($file -like "http*") { + $Device.devicetype = "URL" + $Restore.CredentialName = $AzureCredential + } + else { + $Device.devicetype = "File" + } + $Restore.FileNumber = $backup.Position + $Restore.Devices.Add($Device) + } + Write-Message -Level Verbose -Message "Performing restore action" + $ConfirmMessage = "`n Restore Database $Database on $SqlInstance `n from files: $RestoreFileNames `n with these file moves: `n $LogicalFileMovesString `n $ConfirmPointInTime `n" + if ($Pscmdlet.ShouldProcess("$Database on $SqlInstance `n `n", $ConfirmMessage)) { + try { + $RestoreComplete = $true + if ($KeepCDC -and $Restore.NoRecovery -eq $false) { + $script = $Restore.Script($server) + if ($script -like '*WITH*') { + $script = $script.TrimEnd() + ' , KEEP_CDC' + } + else { + $script = $script.TrimEnd() + ' WITH KEEP_CDC' + } + if ($true -ne $OutputScriptOnly) { + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $null = $server.ConnectionContext.ExecuteNonQuery($script) + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + } + } + elseif ($null -ne $Pages -and $Action -eq 'Database') { + $script = $Restore.Script($server) + $script = $script -replace "] FROM", "] PAGE='$pages' FROM" + if ($true -ne $OutputScriptOnly) { + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $null = $server.ConnectionContext.ExecuteNonQuery($script) + Write-Progress -id 2 -activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + } + } + elseif ($OutputScriptOnly) { + $script = $Restore.Script($server) + } + elseif ($VerifyOnly) { + Write-Message -Message "VerifyOnly restore" -Level Verbose + Write-Progress -id 2 -activity "Verifying $Database backup file on $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $Verify = $Restore.SqlVerify($server) + Write-Progress -id 2 -activity "Verifying $Database backup file on $sqlinstance - Backup $BackupCnt of $($Backups.count)" -status "Complete" -Completed + if ($verify -eq $true) { + Write-Message -Message "VerifyOnly restore Succeeded" -Level Verbose + return "Verify successful" + } + else { + Write-Message -Message "VerifyOnly restore Failed" -Level Verbose + return "Verify failed" + } + } + else { + $outerProgress = $BackupCnt/$Backups.Count*100 + if ($BackupCnt -eq 1) { + Write-Progress -id 2 -ParentId 1 -Activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete 0 + } + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -percentcomplete 0 + $script = $Restore.Script($Server) + $percentcomplete = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $Restore.add_PercentComplete($percentcomplete) + $Restore.PercentCompleteNotification = 1 + $Restore.SqlRestore($Server) + Write-Progress -id 3 -ParentId 2 -Activity "Restore $($backup.FullName -Join ',')" -Completed + Write-Progress -id 2 -ParentId 1 -Activity "Restoring $Database to $sqlinstance - Backup $BackupCnt of $($Backups.count)" -percentcomplete $outerProgress -status ([System.String]::Format("Progress: {0:N2} %", $outerProgress)) + } + } + catch { + Write-Message -Level Verbose -Message "Failed, Closing Server connection" + $RestoreComplete = $False + $ExitError = $_.Exception.InnerException + Stop-Function -Message "Failed to restore db $Database, stopping" -ErrorRecord $_ + return + } + finally { + + if ($OutputScriptOnly -eq $false) { + [PSCustomObject]@{ + SqlInstance = $SqlInstance + DatabaseName = $backup.Database + DatabaseOwner = $server.ConnectionContext.TrueLogin + NoRecovery = $Restore.NoRecovery + WithReplace = $WithReplace + RestoreComplete = $RestoreComplete + BackupFilesCount = $backup.FullName.Count + RestoredFilesCount = $backup.Filelist.PhysicalName.count + BackupSizeMB = if ([bool]($backup.psobject.Properties.Name -contains 'TotalSize')) { [Math]::Round(($backup | Measure-Object -Property TotalSize -Sum).Sum / 1mb, 2) } else { $null } + CompressedBackupSizeMB = if ([bool]($backup.psobject.Properties.Name -contains 'CompressedBackupSize')) { [Math]::Round(($backup | Measure-Object -Property CompressedBackupSize -Sum).Sum / 1mb, 2) } else { $null } + BackupFile = $backup.FullName -Join ',' + RestoredFile = $((Split-Path $backup.FileList.PhysicalName -Leaf) | Sort-Object -Unique) -Join ',' + RestoredFileFull = ($backup.Filelist.PhysicalName -Join ',') + RestoreDirectory = ((Split-Path $backup.FileList.PhysicalName) | Sort-Object -Unique) -Join ',' + BackupSize = if ([bool]($backup.psobject.Properties.Name -contains 'TotalSize')) { ($backup | Measure-Object -Property TotalSize -Sum).Sum } else { $null } + CompressedBackupSize = if ([bool]($backup.psobject.Properties.Name -contains 'CompressedBackupSize')) { ($backup | Measure-Object -Property CompressedBackupSize -Sum).Sum } else { $null } + Script = $script + BackupFileRaw = ($backups.Fullname) + ExitError = $ExitError + } | Select-DefaultView -ExcludeProperty BackupSize, CompressedBackupSize, ExitError, BackupFileRaw, RestoredFileFull + } + else { + $script + } + if ($Restore.Devices.Count -gt 0) { + $Restore.Devices.Clear() + } + Write-Message -Level Verbose -Message "Succeeded, Closing Server connection" + $server.ConnectionContext.Disconnect() + } + } + Write-Progress -id 1 -Activity "Restoring" -Completed + Write-Progress -id 2 -Activity "Restoring" -Completed + $BackupCnt++ + } + if ($server.ConnectionContext.exists) { + $server.ConnectionContext.Disconnect() + } + } + } +} diff --git a/functions/Invoke-DbaBalanceDataFiles.ps1 b/functions/Invoke-DbaBalanceDataFiles.ps1 new file mode 100644 index 000000000000..abb72e7a65e0 --- /dev/null +++ b/functions/Invoke-DbaBalanceDataFiles.ps1 @@ -0,0 +1,365 @@ +function Invoke-DbaBalanceDataFiles { + <# + .SYNOPSIS + Re-balance data between data files + + .DESCRIPTION + When you have a large database with a single data file and add another file, SQL Server will only use the new file until it's about the same size. + You may want to balance the data between all the data files. + + The function will check the server version and edition to see if the it allows for online index rebuilds. + If the server does support it, it will try to rebuild the index online. + If the server doesn't support it, it will rebuild the index offline. Be carefull though, this can cause downtime + + The tables must have a clustered index to be able to balance out the data. + The function does NOT yet support heaps. + + The function will also check if the file groups are subject to balance out. + A file group whould have at least have 2 data files and should be writable. + If a table is within such a file group it will be subject for processing. If not the table will be skipped. + + .PARAMETER SqlInstance + The target SQL Server instance or instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. + + .PARAMETER Table + The tables(s) of the database to process. If unspecified, all tables will be processed. + + .PARAMETER RebuildOffline + Will set all the indexes to rebuild offline. + This option is also needed when the server version is below 2005. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + The server does not support online rebuilds of indexes. + Do you want to rebuild the indexes offline? + [Y] Yes [N] No [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + This will disable the check for enough disk space for the action to be successful. + Use this with caution!! + + .NOTES + Tags: Database, FileManagement, File, Space + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 + + This command will distribute the data in database db1 on instance sql1 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 | Select-Object -ExpandProperty DataFilesEnd + + This command will distribute the data in database db1 on instance sql1 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 -Table table1,table2,table5 + + This command will distribute the data for only the tables table1,table2 and table5 + + .EXAMPLE + Invoke-DbaBalanceDataFiles -SqlInstance sql1 -Database db1 -RebuildOffline + + This command will consider the fact that there might be a SQL Server edition that does not support online rebuilds of indexes. + By supplying this parameter you give permission to do the rebuilds offline if the edition does not support it. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(ParameterSetName = "Pipe", Mandatory = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [Alias("Tables")] + [object[]]$Table, + [switch]$RebuildOffline, + [switch]$EnableException, + [switch]$Force + ) + + process { + + Write-Message -Message "Starting balancing out data files" -Level Verbose + + # Set the initial success flag + [bool]$success = $true + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Check the database parameter + if ($Database) { + if ($Database -notin $server.Databases.Name) { + Stop-Function -Message "One or more databases cannot be found on instance on instance $instance" -Target $instance -Continue + } + + $DatabaseCollection = $server.Databases | Where-Object { $_.Name -in $Database } + } + else { + Stop-Function -Message "Please supply a database to balance out" -Target $instance -Continue + } + + # Get the server version + $serverVersion = $server.Version.Major + + # Check edition of the sql instance + if ($RebuildOffline) { + Write-Message -Message "Continuing with offline rebuild." -Level Verbose + } + elseif (-not $RebuildOffline -and ($serverVersion -lt 9 -or (([string]$Server.Edition -notmatch "Developer") -and ($Server.Edition -notmatch "Enterprise")))) { + # Set up the confirm part + $message = "The server does not support online rebuilds of indexes. `nDo you want to rebuild the indexes offline?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Set the option to generate a full backup + Write-Message -Message "Continuing with offline rebuild." -Level Verbose + + [bool]$supportOnlineRebuild = $false + } + 1 { + Stop-Function -Message "You chose to not allow offline rebuilds of indexes. Use -RebuildOffline" -Target $instance + return + } + } # switch + } + elseif ($serverVersion -ge 9 -and (([string]$Server.Edition -like "Developer*") -or ($Server.Edition -like "Enterprise*"))) { + [bool]$supportOnlineRebuild = $true + } + + # Loop through each of the databases + foreach ($db in $DatabaseCollection) { + $dataFilesStarting = Get-DbaDatabaseFile -SqlInstance $server -Database $db.Name | Where-Object { $_.TypeDescription -eq 'ROWS' } | Select-Object ID, LogicalName, PhysicalName, Size, UsedSpace, AvailableSpace | Sort-Object ID + + if (-not $Force) { + # Check the amount of disk space available + $query = "SELECT SUBSTRING(physical_name, 0, 4) AS 'Drive' , + SUM(( size * 8 ) / 1024) AS 'SizeMB' + FROM sys.master_files + WHERE DB_NAME(database_id) = '$($db.Name)' + GROUP BY SUBSTRING(physical_name, 0, 4)" + # Execute the query + $dbDiskUsage = $Server.Query($query) + + # Get the free space for each drive + $result = $Server.Query("xp_fixeddrives") + $MbFreeColName = $result[0].psobject.Properties.Name[1] + $diskFreeSpace = $result | Select-Object Drive, @{ Name = 'FreeMB'; Expression = { $_.$MbFreeColName } } + + # Loop through each of the drives to see if the size of files on that + # particular disk do not exceed the free space of that disk + foreach ($d in $dbDiskUsage) { + $freeSpace = $diskFreeSpace | Where-Object { $_.Drive -eq $d.Drive.Trim(':\') } | Select-Object FreeMB + if ($d.SizeMB -gt $freeSpace.FreeMB) { + # Set the success flag + $success = $false + + Stop-Function -Message "The available space may not be sufficient to continue the process. Please use -Force to try anyway." -Target $instance -Continue + return + } + } + } + + # Create the start time + $start = Get-Date + + # Check if the function needs to continue + if ($success) { + + # Get the database files before all the alterations + Write-Message -Message "Retrieving data files before data move" -Level Verbose + Write-Message -Message "Processing database $db" -Level Verbose + + # Check the datafiles of the database + $dataFiles = Get-DbaDatabaseFile -SqlInstance $instance -Database $db | Where-Object { $_.TypeDescription -eq 'ROWS' } + if ($dataFiles.Count -eq 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "Database $db only has one data file. Please add a data file to balance out the data" -Target $instance -Continue + } + + # Check the tables parameter + if ($Table) { + if ($Table -notin $db.Table) { + # Set the success flag + $success = $false + + Stop-Function -Message "One or more tables cannot be found in database $db on instance $instance" -Target $instance -Continue + } + + $TableCollection = $db.Tables | Where-Object { $_.Name -in $Table } + } + else { + $TableCollection = $db.Tables + } + + # Get the database file groups and check the aount of data files + Write-Message -Message "Retrieving file groups" -Level Verbose + $fileGroups = $Server.Databases[$db.Name].FileGroups + + # ARray to hold the file groups with properties + $balanceableTables = @() + + # Loop through each of the file groups + + foreach ($fg in $fileGroups) { + + # If there is less than 2 files balancing out data is not possible + if (($fg.Files.Count -ge 2) -and ($fg.Readonly -eq $false)) { + $balanceableTables += $fg.EnumObjects() | Where-Object { $_.GetType().Name -eq 'Table' } + } + } + + $unsuccessfulTables = @() + + # Loop through each of the tables + foreach ($tbl in $TableCollection) { + + # Chck if the table balanceable + if ($tbl.Name -in $balanceableTables.Name) { + + Write-Message -Message "Processing table $tbl" -Level Verbose + + # Chck the tables and get the clustered indexes + if ($TableCollection.Indexes.Count -lt 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "Table $tbl does not contain any indexes" -Target $instance -Continue + } + else { + + # Get all the clustered indexes for the table + $clusteredIndexes = $TableCollection.Indexes | Where-Object { $_.IndexType -eq 'ClusteredIndex' } + + if ($clusteredIndexes.Count -lt 1) { + # Set the success flag + $success = $false + + Stop-Function -Message "No clustered indexes found in table $tbl" -Target $instance -Continue + } + } + + # Loop through each of the clustered indexes and rebuild them + Write-Message -Message "$($clusteredIndexes.Count) clustered index(es) found for table $tbl" -Level Verbose + if ($PSCmdlet.ShouldProcess("Rebuilding indexes to balance data")) { + foreach ($ci in $clusteredIndexes) { + + Write-Message -Message "Rebuilding index $($ci.Name)" -Level Verbose + + # Get the original index operation + [bool]$originalIndexOperation = $ci.OnlineIndexOperation + + # Set the rebuild option to be either offline or online + if ($RebuildOffline) { + $ci.OnlineIndexOperation = $false + } + elseif ($serverVersion -ge 9 -and $supportOnlineRebuild -and -not $RebuildOffline) { + Write-Message -Message "Setting the index operation for index $($ci.Name) to online" -Level Verbose + $ci.OnlineIndexOperation = $true + } + + # Rebuild the index + try { + Write-Message -Message "Rebuilding index $($ci.Name)" -Level Verbose + $ci.Rebuild() + + # Set the success flag + $success = $true + } + catch { + # Set the original index operation back for the index + $ci.OnlineIndexOperation = $originalIndexOperation + + # Set the success flag + $success = $false + + Stop-Function -Message "Something went wrong rebuilding index $($ci.Name). `n$($_.Exception.Message)" -ErrorRecord $_ -Target $instance -Continue + } + + # Set the original index operation back for the index + Write-Message -Message "Setting the index operation for index $($ci.Name) back to the original value" -Level Verbose + $ci.OnlineIndexOperation = $originalIndexOperation + + } # foreach index + + } # if process + + } # if table is balanceable + else { + # Add the table to the unsuccessful array + $unsuccessfulTables += $tbl.Name + + # Set the success flag + $success = $false + + Write-Message -Message "Table $tbl cannot be balanced out" -Level Verbose + } + + } #foreach table + } + + # Create the end time + $end = Get-Date + + # Create the time span + $timespan = New-TimeSpan -Start $start -End $end + $ts = [timespan]::fromseconds($timespan.TotalSeconds) + $elapsed = "{0:HH:mm:ss}" -f ([datetime]$ts.Ticks) + + # Get the database files after all the alterations + Write-Message -Message "Retrieving data files after data move" -Level Verbose + $dataFilesEnding = Get-DbaDatabaseFile -SqlInstance $server -Database $db.Name | Where-Object { $_.TypeDescription -eq 'ROWS' } | Select-Object ID, LogicalName, PhysicalName, Size, UsedSpace, AvailableSpace | Sort-Object ID + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Start = $start + End = $end + Elapsed = $elapsed + Success = $success + Unsuccessful = $unsuccessfulTables -join "," + DataFilesStart = $dataFilesStarting + DataFilesEnd = $dataFilesEnding + } + + } # foreach database + + } # end process + } +} diff --git a/functions/Invoke-DbaCycleErrorLog.ps1 b/functions/Invoke-DbaCycleErrorLog.ps1 new file mode 100644 index 000000000000..3b8185caa4a8 --- /dev/null +++ b/functions/Invoke-DbaCycleErrorLog.ps1 @@ -0,0 +1,134 @@ +function Invoke-DbaCycleErrorLog { + <# + .SYNOPSIS + Cycles the current instance or agent log. + + .DESCRIPTION + Cycles the current error log for the instance (SQL Server) and/or SQL Server Agent. + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Type + The log to cycle. + Accepts: instance or agent. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Log, Cycle + Author: Shawn Melton (@wsmelton | http://blog.wsmelton.info) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaCycleLog + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 -Type agent + + Cycles the current error log for the SQL Server Agent on SQL Server instance sql2016 + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 -Type instance + + Cycles the current error log for the SQL Server instance on SQL Server instance sql2016 + + .EXAMPLE + Invoke-DbaCycleLog -SqlInstance sql2016 + + Cycles the current error log for both SQL Server instance and SQL Server Agent on SQL Server instance sql2016 + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [ValidateSet('instance', 'agent')] + [string]$Type, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (Test-Bound 'Type') { + if ($Type -notin 'instance', 'agent') { + Stop-Function -Message "The type provided [$Type] for $SqlInstance is not an accepted value. Please use 'Instance' or 'Agent'" + return + } + } + $logToCycle = @() + switch ($Type) { + 'agent' { + $sql = "EXEC msdb.dbo.sp_cycle_agent_errorlog;" + $logToCycle = $Type + } + 'instance' { + $sql = "EXEC master.dbo.sp_cycle_errorlog;" + $logToCycle = $Type + } + default { + $sql = " + EXEC master.dbo.sp_cycle_errorlog; + EXEC msdb.dbo.sp_cycle_agent_errorlog;" + $logToCycle = 'instance', 'agent' + } + } + + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $logs = $logToCycle -join ',' + if ($Pscmdlet.ShouldProcess($server, "Cycle the log(s): $logs")) { + $null = $server.Query($sql) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LogType = $logToCycle + IsSuccessful = $true + Notes = $null + } + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LogType = $logToCycle + IsSuccessful = $false + Notes = $_.Exception + } + Stop-Function -Message "Issue cycling $logs on $server" -Target $server -ErrorRecord $_ -Exception $_.Exception -Continue + } + } + } +} diff --git a/functions/Invoke-DbaDatabaseClone.ps1 b/functions/Invoke-DbaDatabaseClone.ps1 new file mode 100644 index 000000000000..ed18da4a2b86 --- /dev/null +++ b/functions/Invoke-DbaDatabaseClone.ps1 @@ -0,0 +1,188 @@ +function Invoke-DbaDatabaseClone { + <# + .SYNOPSIS + Clones a database schema and statistics + + .DESCRIPTION + Clones a database schema and statistics. + + This can be useful for testing query performance without requiring all the space needed for the data in the database. + + Read more at sqlperformance: https://sqlperformance.com/2016/08/sql-statistics/expanding-dbcc-clonedatabase + + Thanks to Microsoft Tiger Team for the code and idea https://github.com/Microsoft/tigertoolbox/ + + .PARAMETER SqlInstance + Allows you to specify a comma separated list of servers to query. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to clone - this list is auto-populated from the server. + + .PARAMETER CloneDatabase + The name(s) to clone to. + + .PARAMETER UpdateStatistics + Update the statistics prior to cloning (per Microsoft Tiger Team formula) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Statistics, Performance + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDatabaseClone + + .EXAMPLE + Invoke-DbaDatabaseClone -SqlInstance sql2016 -Database mydb -CloneDatabase myclone + Clones mydb to myclone on sql2016 + + .EXAMPLE + Invoke-DbaDatabaseClone -SqlInstance sql2016 -Database mydb -CloneDatabase myclone, myclone2 -UpdateStatistics + Updates the statistics of mydb then clones to myclone and myclone2 + + #> + [CmdletBinding()] + param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer", "SqlServers")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory, ValueFromPipeline)] + [object]$Database, + [string[]]$CloneDatabase, + [switch]$UpdateStatistics, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if (-not $Database.Name -and -not $SqlInstance) { + Stop-Function -Message "You must specify a server name if you did not pipe a database" + } + + $updatestats = "declare @out table(id int identity(1,1),s sysname, o sysname, i sysname, stats_stream varbinary(max), rows bigint, pages bigint) + declare @dbcc table(stats_stream varbinary(max), rows bigint, pages bigint) + declare c cursor for + select object_schema_name(object_id) s, object_name(object_id) o, name i + from sys.indexes + where type_desc in ('CLUSTERED COLUMNSTORE', 'NONCLUSTERED COLUMNSTORE') + declare @s sysname, @o sysname, @i sysname + open c + fetch next from c into @s, @o, @i + while @@FETCH_STATUS = 0 begin + declare @showStats nvarchar(max) = N'DBCC SHOW_STATISTICS(""' + quotename(@s) + '.' + quotename(@o) + '"", ' + quotename(@i) + ') with stats_stream' + insert @dbcc exec sp_executesql @showStats + insert @out select @s, @o, @i, stats_stream, rows, pages from @dbcc + delete @dbcc + fetch next from c into @s, @o, @i + end + close c + deallocate c + + + declare @sql nvarchar(max); + declare @id int; + + select top 1 @id=id,@sql= + 'UPDATE STATISTICS ' + quotename(s) + '.' + quotename(o) + '(' + quotename(i) + + ') with stats_stream = ' + convert(nvarchar(max), stats_stream, 1) + + ', rowcount = ' + convert(nvarchar(max), rows) + ', pagecount = ' + convert(nvarchar(max), pages) + from @out + + WHILE (@@ROWCOUNT <> 0) + BEGIN + exec sp_executesql @sql + delete @out where id = @id + select top 1 @id=id,@sql= + 'UPDATE STATISTICS ' + quotename(s) + '.' + quotename(o) + '(' + quotename(i) + + ') with stats_stream = ' + convert(nvarchar(max), stats_stream, 1) + + ', rowcount = ' + convert(nvarchar(max), rows) + ', pagecount = ' + convert(nvarchar(max), pages) + from @out + END" + + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 12 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $sql2012min = [version]"11.0.7001.0" # SQL 2012 SP4 + $sql2014min = [version]"12.0.5000.0" # SQL 2014 SP2 + $sql2016min = [version]"13.0.4001.0" # SQL 2016 SP1 + + + if ($server.VersionMajor -eq 11 -and $server.Version -lt $sql2012min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2012 SP4 and above required." -Target $server -Continue + } + + if ($server.VersionMajor -eq 12 -and $server.Version -lt $sql2014min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2014 SP2 and above required." -Target $server -Continue + } + + if ($server.VersionMajor -eq 13 -and $server.Version -lt $sql2016min) { + Stop-Function -Message "Unsupported version for $instance. SQL Server 2016 SP1 and above required." -Target $server -Continue + } + + if (-not $Database.Name) { + [Microsoft.SqlServer.Management.Smo.Database]$database = $server.Databases[$database] + } + + if ($Database.IsSystemObject) { + Stop-Function -Message "Only user databases are supported" -Target $instance -Continue + } + + if (-not $Database.name) { + Stop-Function -Message "Database not found" -Target $instance -Continue + } + + if ($UpdateStatistics) { + try { + Write-Message -Level Verbose -Message "Updating statistics" + $null = $database.Query($updatestats) + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + } + + $dbname = $database.Name + + foreach ($db in $CloneDatabase) { + Write-Message -Level Verbose -Message "Cloning $db from $database" + if ($server.Databases[$db]) { + Stop-Function -Message "Destination clone database $db already exists" -Target $instance -Continue + } + else { + try { + $sql = "dbcc clonedatabase('$dbname','$db')" + $null = $database.Query($sql) + $server.Databases.Refresh() + Get-DbaDatabase -SqlInstance $server -Database $db + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaDatabaseShrink.ps1 b/functions/Invoke-DbaDatabaseShrink.ps1 new file mode 100644 index 000000000000..4d29b303a41b --- /dev/null +++ b/functions/Invoke-DbaDatabaseShrink.ps1 @@ -0,0 +1,348 @@ +function Invoke-DbaDatabaseShrink { + <# + .SYNOPSIS + Shrinks all files in a database. This is a command that should rarely be used. + + - Shrinks can cause severe index fragmentation (to the tune of 99%) + - Shrinks can cause massive growth in the database's transaction log + - Shrinks can require a lot of time and system resources to perform data movement + + .DESCRIPTION + Shrinks all files in a database. Databases should be shrunk only when completely necessary. + + Many awesome SQL people have written about why you should not shrink your data files. Paul Randal and Kalen Delaney wrote great posts about this topic: + + http://www.sqlskills.com/blogs/paul/why-you-should-not-shrink-your-data-files + http://sqlmag.com/sql-server/shrinking-data-files + + However, there are some cases where a database will need to be shrunk. In the event that you must shrink your database: + + 1. Ensure you have plenty of space for your T-Log to grow + 2. Understand that shrinks require a lot of CPU and disk resources + 3. Consider running DBCC INDEXDEFRAG or ALTER INDEX ... REORGANIZE after the shrink is complete. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllUserDatabases + Run command against all user databases + + .PARAMETER PercentFreeSpace + Specifies how much to reduce the database in percent, defaults to 0. + + .PARAMETER ShrinkMethod + Specifies the method that is used to shrink the database + Default + Data in pages located at the end of a file is moved to pages earlier in the file. Files are truncated to reflect allocated space. + EmptyFile + Migrates all of the data from the referenced file to other files in the same filegroup. (DataFile and LogFile objects only). + NoTruncate + Data in pages located at the end of a file is moved to pages earlier in the file. + TruncateOnly + Data distribution is not affected. Files are truncated to reflect allocated space, recovering free space at the end of any file. + + .PARAMETER StatementTimeout + Timeout in minutes. Defaults to infinity (shrinks can take a while.) + + .PARAMETER LogsOnly + Deprecated. Use FileType instead + + .PARAMETER FileType + Specifies the files types that will be shrunk + All + All Data and Log files are shrunk, using database shrink (Default) + Data + Just the Data files are shrunk using file shrink + Log + Just the Log files are shrunk using file shrink + + .PARAMETER ExcludeIndexStats + Exclude statistics about fragmentation + + .PARAMETER ExcludeUpdateUsage + Exclude DBCC UPDATE USAGE for database + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Shrink database" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Shrink, Database + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDatabaseShrink + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2016 -Database Northwind,pubs,Adventureworks2014 + + Shrinks Northwind, pubs and Adventureworks2014 to have as little free space as possible. + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2014 -Database AdventureWorks2014 -PercentFreeSpace 50 + + Shrinks AdventureWorks2014 to have 50% free space. So let's say AdventureWorks2014 was 1GB and it's using 100MB space. The database free space would be reduced to 50MB. + + .EXAMPLE + Invoke-DbaDatabaseShrink -SqlInstance sql2012 -AllUserDatabases + + Shrinks all databases on SQL2012 (not ideal for production) + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllUserDatabases, + [ValidateRange(0, 99)] + [int]$PercentFreeSpace = 0, + [ValidateSet('Default', 'EmptyFile', 'NoTruncate', 'TruncateOnly')] + [string]$ShrinkMethod = "Default", + [ValidateSet('All', 'Data', 'Log')] + [string]$FileType = "All", + [int]$StatementTimeout = 0, + [switch]$LogsOnly, + [switch]$ExcludeIndexStats, + [switch]$ExcludeUpdateUsage, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($LogsOnly) { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Parameter "LogsOnly" + $FileType = 'Log' + } + + $StatementTimeoutSeconds = $StatementTimeout * 60 + + $sql = "SELECT + indexstats.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats + INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id] + INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id] + INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id] + AND indexstats.index_id = dbindexes.index_id + WHERE indexstats.database_id = DB_ID() AND indexstats.avg_fragmentation_in_percent > 0 + ORDER BY indexstats.avg_fragmentation_in_percent desc" + + $sqlTop1 = "SELECT top 1 + indexstats.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats + INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id] + INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id] + INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id] + AND indexstats.index_id = dbindexes.index_id + WHERE indexstats.database_id = DB_ID() + ORDER BY indexstats.avg_fragmentation_in_percent desc" + } + + process { + if (!$Database -and !$ExcludeDatabase -and !$AllUserDatabases) { + Stop-Function -Message "You must specify databases to execute against using either -Databases, -Exclude or -AllUserDatabases" -Continue + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # changing statement timeout to $StatementTimeout + if ($StatementTimeout -eq 0) { + Write-Message -Level Verbose -Message "Changing statement timeout to infinity" + } + else { + Write-Message -Level Verbose -Message "Changing statement timeout to $StatementTimeout minutes" + } + $server.ConnectionContext.StatementTimeout = $StatementTimeoutSeconds + + $dbs = $server.Databases | Where-Object { $_.IsSystemObject -eq $false -and $_.IsAccessible } + + if ($Database) { + $dbs = $dbs | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $db on $instance" + + if ($db.IsDatabaseSnapshot) { + Write-Message -Level Warning -Message "The database $db on server $instance is a snapshot and cannot be shrunk. Skipping database." + continue + } + + $startingSize = $db.Size + $spaceAvailableMB = $db.SpaceAvailable / 1024 + $spaceUsed = $startingSize - $spaceAvailableMB + $desiredSpaceAvailable = ($PercentFreeSpace * $spaceUsed) / 100 + + Write-Message -Level Verbose -Message "Starting Size (MB): $startingSize" + Write-Message -Level Verbose -Message "Starting Freespace (MB): $([int]$spaceAvailableMB)" + Write-Message -Level Verbose -Message "Desired Freespace (MB): $([int]$desiredSpaceAvailable)" + + if (($db.SpaceAvailable / 1024) -le $desiredSpaceAvailable) { + Write-Message -Level Warning -Message "Space Available ($spaceAvailableMB) is less than or equal to the desired outcome ($desiredSpaceAvailable)" + } + else { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Shrinking from $([int]$spaceAvailableMB) MB space available to $([int]$desiredSpaceAvailable) MB space available")) { + if ($db.Tables.Indexes.Name -and $server.VersionMajor -gt 8 -and $ExcludeIndexStats -eq $false) { + Write-Message -Level Verbose -Message "Getting average fragmentation" + $startingFrag = ($server.Query($sql, $db.name) | Select-Object -ExpandProperty avg_fragmentation_in_percent | Measure-Object -Average).Average + $startingTopFrag = ($server.Query($sqlTop1, $db.name)).avg_fragmentation_in_percent + } + else { + $startingTopFrag = $startingFrag = $null + } + + $start = Get-Date + + switch ($FileType) { + 'Log' { + try { + Write-Message -Level Verbose -Message "Beginning shrink of log files" + $db.LogFiles.Shrink($desiredSpaceAvailable, $ShrinkMethod) + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + 'Data' { + try { + Write-Message -Level Verbose -Message "Beginning shrink of data files" + foreach ($fileGroup in $db.FileGroups) { + foreach ($file in $fileGroup.Files) { + Write-Message -Level Verbose -Message "Beginning shrink of $($file.Name)" + $file.Shrink($PercentFreeSpace, $ShrinkMethod) + } + } + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + if (-not $ExcludeUpdateUsage) { $db.RecalculateSpaceUsage() } + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + default { + try { + Write-Message -Level Verbose -Message "Beginning shrink of entire database" + $db.Shrink($desiredSpaceAvailable, $ShrinkMethod) + $db.Refresh() + Write-Message -Level Verbose -Message "Recalculating space usage" + if (-not $ExcludeUpdateUsage) { $db.RecalculateSpaceUsage() } + $success = $true + $notes = $null + } + catch { + $success = $false + $notes = $_.Exception.InnerException + } + } + } + + $end = Get-Date + $dbSize = $db.Size + $newSpaceAvailableMB = $db.SpaceAvailable / 1024 + + Write-Message -Level Verbose -Message "Final database size: $([int]$dbSize) MB" + Write-Message -Level Verbose -Message "Final space available: $([int]$newSpaceAvailableMB) MB" + + if ($db.Tables.Indexes.Name -and $server.VersionMajor -gt 8 -and $ExcludeIndexStats -eq $false) { + Write-Message -Level Verbose -Message "Refreshing indexes and getting average fragmentation" + $endingDefrag = ($server.Query($sql, $db.name) | Select-Object -ExpandProperty avg_fragmentation_in_percent | Measure-Object -Average).Average + $endingTopDefrag = ($server.Query($sqlTop1, $db.name)).avg_fragmentation_in_percent + } + else { + $endingTopDefrag = $endingDefrag = $null + } + + $timSpan = New-TimeSpan -Start $start -End $end + $ts = [TimeSpan]::fromseconds($timSpan.TotalSeconds) + $elapsed = "{0:HH:mm:ss}" -f ([datetime]$ts.Ticks) + } + } + + #$db.TruncateLog() + + if ($Pscmdlet.ShouldProcess("$db on $instance", "Showing results")) { + if ($null -eq $notes) { + $notes = "Database shrinks can cause massive index fragmentation and negatively impact performance. You should now run DBCC INDEXDEFRAG or ALTER INDEX ... REORGANIZE" + } + $object = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Start = $start + End = $end + Elapsed = $elapsed + Success = $success + StartingTotalSizeMB = [math]::Round($startingSize, 2) + StartingUsedMB = [math]::Round($spaceUsed, 2) + FinalTotalSizeMB = [math]::Round($db.size, 2) + StartingAvailableMB = [math]::Round($spaceAvailableMB, 2) + DesiredAvailableMB = [math]::Round($desiredSpaceAvailable, 2) + FinalAvailableMB = [math]::Round(($db.SpaceAvailable / 1024), 2) + StartingAvgIndexFragmentation = [math]::Round($startingFrag, 1) + EndingAvgIndexFragmentation = [math]::Round($endingDefrag, 1) + StartingTopIndexFragmentation = [math]::Round($startingTopFrag, 1) + EndingTopIndexFragmentation = [math]::Round($endingTopDefrag, 1) + Notes = $notes + } + + if ($ExcludeIndexStats) { + Select-DefaultView -InputObject $object -ExcludeProperty StartingAvgIndexFragmentation, EndingAvgIndexFragmentation, StartingTopIndexFragmentation, EndingTopIndexFragmentation + } + else { + $object + } + } + } + } + } +} + diff --git a/functions/Invoke-DbaDatabaseUpgrade.ps1 b/functions/Invoke-DbaDatabaseUpgrade.ps1 new file mode 100644 index 000000000000..ad948eaaa717 --- /dev/null +++ b/functions/Invoke-DbaDatabaseUpgrade.ps1 @@ -0,0 +1,289 @@ +function Invoke-DbaDatabaseUpgrade { + <# + .SYNOPSIS + Take a database and upgrades it to compatibility of the SQL Instance its hosted on. Based on https://thomaslarock.com/2014/06/upgrading-to-sql-server-2014-a-dozen-things-to-check/ + + .DESCRIPTION + Updates compatibility level, then runs CHECKDB with data_purity, DBCC updateusage, sp_updatestats and finally sp_refreshview against all user views. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is autopopulated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is autopopulated from the server + + .PARAMETER AllUserDatabases + Run command against all user databases + + .PARAMETER Force + Don't skip over databases that are already at the same level the instance is + + .PARAMETER NoCheckDb + Skip checkdb + + .PARAMETER NoUpdateUsage + Skip usage update + + .PARAMETER NoUpdateStats + Skip stats update + + .PARAMETER NoRefreshView + Skip view update + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Update database" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Shrink, Database + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + + .LINK + https://dbatools.io/Invoke-DbaDatabaseUpgrade + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-MSD01 -Database Test + + Runs the below processes against the databases + -- Puts compatibility of database to level of SQL Instance + -- Runs CHECKDB DATA_PURITY + -- Runs DBCC UPDATESUSAGE + -- Updates all users statistics + -- Runs sp_refreshview against every view in the database + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-INT01 -Database Test -NoRefreshView + + Runs the upgrade command skipping the sp_refreshview update on all views + + .EXAMPLE + Invoke-DbaDatabaseUpgrade -SqlInstance PRD-SQL-INT01 -Database Test -Force + + If database Test is already at the correct compatibility, runs every necessary step + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2016 | Out-GridView -Passthru | Invoke-DbaDatabaseUpgrade + + Get only specific databases using GridView and pass those to Invoke-DbaDatabaseUpgrade +#> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [parameter(Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$NoCheckDb, + [switch]$NoUpdateUsage, + [switch]$NoUpdateStats, + [switch]$NoRefreshView, + [switch]$AllUserDatabases, + [switch]$Force, + [parameter(ValueFromPipeline)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + if (Test-Bound -not 'SqlInstance', 'InputObject') { + Write-Message -Level Warning -Message "You must specify either a SQL instance or pipe a database collection" + continue + } + + if (Test-Bound -not 'Database', 'InputObject', 'ExcludeDatabase', 'AllUserDatabases') { + Write-Message -Level Warning -Message "You must explicitly specify a database. Use -Database, -ExcludeDatabase, -AllUserDatabases or pipe a database collection" + continue + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + $server.ConnectionContext.StatementTimeout = [Int32]::MaxValue + } + catch { + Stop-Function -Message "Failed to process Instance $Instance" -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Databases | Where-Object IsAccessible + } + + $InputObject = $InputObject | Where-Object { $_.IsSystemObject -eq $false } + if ($Database) { + $InputObject = $InputObject | Where-Object { $_.Name -contains $Database } + } + if ($ExcludeDatabase) { + $InputObject = $InputObject | Where-Object { $_.Name -notcontains $ExcludeDatabase } + } + + foreach ($db in $InputObject) { + # create objects to use in updates + $server = $db.Parent + $ServerVersion = $server.VersionMajor + Write-Message -Level Verbose -Message "SQL Server is using Version: $ServerVersion" + + $ogcompat = $db.CompatibilityLevel + $dbname = $db.Name + $dbversion = switch ($db.CompatibilityLevel) { + "Version100" { 10 } # SQL Server 2008 + "Version110" { 11 } # SQL Server 2012 + "Version120" { 12 } # SQL Server 2014 + "Version130" { 13 } # SQL Server 2016 + "Version140" { 14 } # SQL Server 2017 + default { 9 } # SQL Server 2005 + } + if (-not $Force) { + # skip over databases at the correct level, unless -Force + if ($dbversion -ge $ServerVersion) { + Write-Message -Level VeryVerbose -Message "Skipping $db because compatibility is at the correct level. Use -Force if you want to run all the additional steps" + continue + } + } + Write-Message -Level Verbose -Message "Updating $db compatibility to SQL Instance level" + if ($dbversion -lt $ServerVersion) { + If ($Pscmdlet.ShouldProcess($server, "Updating $db version on $server from $dbversion to $ServerVersion")) { + $Comp = $ServerVersion * 10 + $tsqlComp = "ALTER DATABASE $db SET COMPATIBILITY_LEVEL = $Comp" + try { + $db.ExecuteNonQuery($tsqlComp) + $comResult = $Comp + } + catch { + Write-Message -Level Warning -Message "Failed run Compatibility Upgrade" -ErrorRecord $_ -Target $instance + $comResult = "Fail" + } + } + } + else { + $comResult = "No change" + } + + if (!($NoCheckDb)) { + Write-Message -Level Verbose -Message "Updating $db with DBCC CHECKDB DATA_PURITY" + If ($Pscmdlet.ShouldProcess($server, "Updating $db with DBCC CHECKDB DATA_PURITY")) { + $tsqlCheckDB = "DBCC CHECKDB ('$dbname') WITH DATA_PURITY, NO_INFOMSGS" + try { + $db.ExecuteNonQuery($tsqlCheckDB) + $DataPurityResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed run DBCC CHECKDB with DATA_PURITY on $db" -ErrorRecord $_ -Target $instance + $DataPurityResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignoring CHECKDB DATA_PURITY" + } + + if (!($NoUpdateUsage)) { + Write-Message -Level Verbose -Message "Updating $db with DBCC UPDATEUSAGE" + If ($Pscmdlet.ShouldProcess($server, "Updating $db with DBCC UPDATEUSAGE")) { + $tsqlUpdateUsage = "DBCC UPDATEUSAGE ($db) WITH NO_INFOMSGS;" + try { + $db.ExecuteNonQuery($tsqlUpdateUsage) + $UpdateUsageResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed to run DBCC UPDATEUSAGE on $db" -ErrorRecord $_ -Target $instance + $UpdateUsageResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignore DBCC UPDATEUSAGE" + $UpdateUsageResult = "Skipped" + } + + if (!($NoUpdatestats)) { + Write-Message -Level Verbose -Message "Updating $db statistics" + If ($Pscmdlet.ShouldProcess($server, "Updating $db statistics")) { + $tsqlStats = "EXEC sp_updatestats;" + try { + $db.ExecuteNonQuery($tsqlStats) + $UpdateStatsResult = "Success" + } + catch { + Write-Message -Level Warning -Message "Failed to run sp_updatestats on $db" -ErrorRecord $_ -Target $instance + $UpdateStatsResult = "Fail" + } + } + } + else { + Write-Message -Level Verbose -Message "Ignoring sp_updatestats" + $UpdateStatsResult = "Skipped" + } + + if (!($NoRefreshView)) { + Write-Message -Level Verbose -Message "Refreshing $db Views" + $dbViews = $db.Views | Where-Object IsSystemObject -eq $false + $RefreshViewResult = "Success" + foreach ($dbview in $dbviews) { + $viewName = $dbView.Name + $viewSchema = $dbView.Schema + $fullName = $viewSchema + "." + $viewName + + $tsqlupdateView = "EXECUTE sp_refreshview N'$fullName'; " + + If ($Pscmdlet.ShouldProcess($server, "Refreshing view $fullName on $db")) { + try { + $db.ExecuteNonQuery($tsqlupdateView) + } + catch { + Write-Message -Level Warning -Message "Failed update view $fullName on $db" -ErrorRecord $_ -Target $instance + $RefreshViewResult = "Fail" + } + } + } + } + else { + Write-Message -Level Verbose -Message "Ignore View Refreshes" + $RefreshViewResult = "Skipped" + } + + If ($Pscmdlet.ShouldProcess("console", "Outputting object")) { + $db.Refresh() + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OriginalCompatibility = $ogcompat.ToString().Replace('Version', '') + CurrentCompatibility = $db.CompatibilityLevel.ToString().Replace('Version', '') + Compatibility = $comResult + DataPurity = $DataPurityResult + UpdateUsage = $UpdateUsageResult + UpdateStats = $UpdateStatsResult + RefreshViews = $RefreshViewResult + } + } + } + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaDbDecryptObject.ps1 b/functions/Invoke-DbaDbDecryptObject.ps1 new file mode 100644 index 000000000000..6c2b6304cf4c --- /dev/null +++ b/functions/Invoke-DbaDbDecryptObject.ps1 @@ -0,0 +1,361 @@ +function Invoke-DbaDbDecryptObject { + <# + .SYNOPSIS + Invoke-DbaDbDecryptObject returns the decrypted version of an object + + .DESCRIPTION + When a procedure or a function is created with encryption and you lost the code you're in trouble. + You cannot alter the object or view the definition. + With this command you can search for the object and decrypt the it. + + The command will output the results to the console. + There is an option to export all the results to a folder creating .sql files. + + Make sure the instance allowed dedicated administrator connections (DAC). + The binary versions of the objects can only be retrieved using a DAC connection. + You can check the DAC connection with: + 'Get-DbaSpConfigure -SqlInstance [yourinstance] -ConfigName RemoteDacConnectionsEnabled' + It should say 1 in the ConfiguredValue. + + To change the configurations you can use the Set-DbaSpConfigure command: + 'Set-DbaSpConfigure -SqlInstance [yourinstance] -ConfigName RemoteDacConnectionsEnabled -Value 1' + In some cases you may need to reboot the instance. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Database to look through for the object. + + .PARAMETER ObjectName + The name of the object to search for in the database. + + .PARAMETER EncodingType + The encoding that's used to decrypt and encrypt values. + + .PARAMETER ExportDestination + Used for exporting the results to. + The destiation will use the instance name, database name and object type i.e.: C:\temp\decrypt\SQLDB1\DB1\StoredProcedure + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Encryption, Decrypt, Database + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDbDecryptObject + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1 + + Decrypt object "Function1" in DB1 of instance SQLDB1 and output the data to the user. + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1 -ExportDestination C:\temp\decrypt + + Decrypt object "Function1" in DB1 of instance SQLDB1 and output the data to the folder "C:\temp\decrypt". + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ExportDestination C:\temp\decrypt + + Decrypt all objects in DB1 of instance SQLDB1 and output the data to the folder "C:\temp\decrypt" + + .EXAMPLE + Invoke-DbaDbDecryptObject -SqlInstance SQLDB1 -Database DB1 -ObjectName Function1, Function2 + + Decrypt objects "Function1" and "Function2" and output the data to the user. + + .EXAMPLE + "SQLDB1" | Invoke-DbaDbDecryptObject -Database DB1 -ObjectName Function1, Function2 + + Decrypt objects "Function1" and "Function2" and output the data to the user using a pipeline for the instance. + #> + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [object[]]$Database, + [string[]]$ObjectName, + [ValidateSet('ASCII', 'UTF8')] + [string]$EncodingType = 'ASCII', + [string]$ExportDestination, + [switch]$EnableException + ) + + begin { + + function Invoke-DecryptData() { + param( + [parameter(Mandatory = $true)] + [byte[]]$Secret, + [parameter(Mandatory = $true)] + [byte[]]$KnownPlain, + [parameter(Mandatory = $true)] + [byte[]]$KnownSecret + ) + + # Declare pointers + [int]$i = 0 + + # Loop through each of the characters and apply an XOR to decrypt the data + $result = $( + + # Loop through the byte string + while ($i -lt $Secret.Length) { + + # Compare the byte string character to the key character using XOR + if ($i -lt $Secret.Length) { + $Secret[$i] -bxor $KnownPlain[$i] -bxor $KnownSecret[$i] + } + + # Increment the byte string indicator + $i += 2 + + } # end while loop + + ) # end data value + + # Get the string value from the data + $decryptedData = $Encoding.GetString($result) + + # Return the decrypted data + return $decryptedData + } + + # Create array list to hold the results + $objectCollection = New-Object System.Collections.ArrayList + + # Set the encoding + if ($EncodingType -eq 'ASCII') { + $encoding = [System.Text.Encoding]::ASCII + } + elseif ($EncodingType -eq 'UTF8') { + $encoding = [System.Text.Encoding]::UTF8 + } + + # Check the export parameter + if ($ExportDestination -and -not (Test-Path $ExportDestination)) { + try { + # Create the new destination + New-Item -Path $ExportDestination -ItemType Directory -Force | Out-Null + } + catch { + Stop-Function -Message "Couldn't create destination folder $ExportDestination" -ErrorRecord $_ -Target $instance -Continue + } + } + + } + + process { + + if (Test-FunctionInterrupt) { return } + + # Loop through all the instances + foreach ($instance in $SqlInstance) { + + # Check the configuration of the intance to see if the DAC is enabled + $config = Get-DbaSpConfigure -SqlInstance $instance -ConfigName RemoteDacConnectionsEnabled + if ($config.ConfiguredValue -ne 1) { + Stop-Function -Message "DAC is not enabled for instance $instance.`nPlease use 'Set-DbaSpConfigure -SqlInstance $instance -ConfigName RemoteDacConnectionsEnabled -Value 1' to configure the instance to allow DAC connections" -Target $instance -Continue + } + + # Try to connect to instance + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = New-Object Microsoft.SqlServer.Management.Smo.Server "ADMIN:$instance" + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Get all the databases that compare to the database parameter + $databaseCollection = $server.Databases | Where-Object {$_.Name -in $Database} + + # Loop through each of databases + foreach ($db in $databaseCollection) { + # Get the objects + if($ObjectName){ + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + } + else{ + # Get all encrypted objects + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + } + + <# Get all the objects + $storedProcedures = @($db.StoredProcedures | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'StoredProcedure'}}, @{N = "SubType"; E = {''}}) + $functions = @($db.UserDefinedFunctions | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {"UserDefinedFunction"}}, @{N = "SubType"; E = {$_.FunctionType.ToString().Trim()}}) + $views = @($db.Views | Where-Object {$_.Name -in $ObjectName -and $_.IsEncrypted -eq $true} | Select-Object Name, Schema, @{N = "ObjectType"; E = {'View'}}, @{N = "SubType"; E = {''}}) + #> + + # Check if there are any objects + if ($storedProcedures.Count -ge 1) { + $objectCollection += $storedProcedures + } + if ($functions.Count -ge 1) { + $objectCollection += $functions + } + if ($views.Count -ge 1) { + $objectCollection += $views + } + + # Loop through all the objects + foreach ($object in $objectCollection) { + + # Setup the query to get the secret + $querySecret = "SELECT imageval AS Value FROM sys.sysobjvalues WHERE objid = OBJECT_ID('$($object.Name)')" + + # Get the result of the secret query + try { + $secret = $server.Databases[$db.Name].Query($querySecret) + } + catch { + Stop-Function -Message "Couldn't retrieve secret from $instance" -ErrorRecord $_ -Target $instance -Continue + } + + # Check if at least a value came back + if ($secret) { + + # Setup a known plain command and get the binary version of it + switch ($object.ObjectType) { + + 'StoredProcedure' { + $queryKnownPlain = (" " * $secret.Value.Length) + "ALTER PROCEDURE $($object.Schema).$($object.Name) WITH ENCRYPTION AS RETURN 0;" + } + 'UserDefinedFunction' { + + switch ($object.SubType) { + 'Inline' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS TABLE WITH ENCRYPTION AS BEGIN RETURN SELECT 0 i END;" + } + 'Scalar' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS INT WITH ENCRYPTION AS BEGIN RETURN 0 END;" + } + 'Table' { + $queryKnownPlain = (" " * $secret.value.length) + "ALTER FUNCTION $($object.Schema).$($object.Name)() RETURNS @r TABLE(i INT) WITH ENCRYPTION AS BEGIN RETURN END;" + } + } + } + 'View' { + $queryKnownPlain = (" " * $secret.Value.Length) + "ALTER VIEW $($object.Schema).$($object.Name) WITH ENCRYPTION AS SELECT NULL AS [Value];" + } + } + + # Convert the known plain into binary + if ($queryKnownPlain) { + try { + $knownPlain = $encoding.GetBytes(($queryKnownPlain)) + } + catch { + Stop-Function -Message "Couldn't convert the known plain to binary" -ErrorRecord $_ -Target $instance -Continue + } + } + else { + Stop-Function -Message "Something went wrong setting up the known plain" -ErrorRecord $_ -Target $instance -Continue + } + + # Setup the query to change the object in SQL Server and roll it back getting the encrypted version + $queryKnownSecret = " + BEGIN TRANSACTION; + EXEC ('$queryKnownPlain'); + SELECT imageval AS Value + FROM sys.sysobjvalues + WHERE objid = OBJECT_ID('$($object.Name)'); + ROLLBACK; + " + + # Get the result for the known encrypted + try { + $knownSecret = $server.Databases[$db.Name].Query($queryKnownSecret) + } + catch { + Stop-Function -Message "Couldn't retrieve known secret from $instance" -ErrorRecord $_ -Target $instance -Continue + } + + # Get the result + $result = Invoke-DecryptData -Secret $secret.value -KnownPlain $knownPlain -KnownSecret $knownSecret.value + + # Check if the results need to be exported + if ($ExportDestination) { + # make up the file name + $filename = "$($object.Schema).$($object.Name).sql" + + # Check the export destination + if ($ExportDestination.EndsWith("\")) { + $destinationFolder = "$ExportDestination$instance\$($db.Name)\$($object.ObjectType)\" + } + else { + $destinationFolder = "$ExportDestination\$instance\$($db.Name)\$($object.ObjectType)\" + } + + # Check if the destination folder exists + if (-not (Test-Path $destinationFolder)) { + try { + # Create the new destination + New-Item -Path $destinationFolder -ItemType Directory -Force:$Force | Out-Null + } + catch { + Stop-Function -Message "Couldn't create destination folder $destinationFolder" -ErrorRecord $_ -Target $instance -Continue + } + } + + # Combine the destination folder and the file name to get the path + $filePath = $destinationFolder + $filename + + # Export the result + try { + $result | Out-File -FilePath $filePath -Force + } + catch { + Stop-Function -Message "Couldn't export the results of $($object.Name) to $filePath" -ErrorRecord $_ -Target $instance -Continue + } + + } + + # Add the results to the custom object + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Type = $object.ObjectType + Schema = $object.Schema + Name = $object.Name + FullName = "$($object.Schema).$($object.Name)" + Script = $result + } + + } # end if secret + + } # end for each object + + } # end for each database + + } # end for each instance + + } # process + + end { + if (Test-FunctionInterrupt) { return } + + Write-Message -Message "Finished decrypting data" -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaDiagnosticQuery.ps1 b/functions/Invoke-DbaDiagnosticQuery.ps1 new file mode 100644 index 000000000000..7e50729ca6cd --- /dev/null +++ b/functions/Invoke-DbaDiagnosticQuery.ps1 @@ -0,0 +1,450 @@ +function Invoke-DbaDiagnosticQuery { + <# + .SYNOPSIS + Invoke-DbaDiagnosticQuery runs the scripts provided by Glenn Berry's DMV scripts on specified servers + + .DESCRIPTION + This is the main function of the Sql Server Diagnostic Queries related functions in dbatools. + The diagnostic queries are developed and maintained by Glenn Berry and they can be found here along with a lot of documentation: + http://www.sqlskills.com/blogs/glenn/category/dmv-queries/ + + The most recent version of the diagnostic queries are included in the dbatools module. + But it is possible to download a newer set or a specific version to an alternative location and parse and run those scripts. + It will run all or a selection of those scripts on one or multiple servers and return the result as a PowerShell Object + + .PARAMETER SqlInstance + The target SQL Server. Can be either a string or SMO server + + .PARAMETER SqlCredential + Allows alternative Windows or SQL login credentials to be used + + .PARAMETER Path + Alternate path for the diagnostic scripts + + .PARAMETER Database + The database(s) to process. If unspecified, all databases will be processed + + .PARAMETER ExcludeDatabase + The database(s) to exclude + + .PARAMETER UseSelectionHelper + Provides a gridview with all the queries to choose from and will run the selection made by the user on the Sql Server instance specified. + + .PARAMETER QueryName + Only run specific query + + .PARAMETER InstanceOnly + Run only instance level queries + + .PARAMETER DatabaseSpecific + Run only database level queries + + .PARAMETER NoQueryTextColumn + Use this switch to exclude the [Complete Query Text] column from relevant queries + + .PARAMETER NoPlanColumn + Use this switch to exclude the [Query Plan] column from relevant queries + + .PARAMETER NoColumnParsing + Does not parse the [Complete Query Text] and [Query Plan] columns and disregards the NoQueryTextColumn and NoColumnParsing switches + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts to confirm certain actions + + .PARAMETER WhatIf + Shows what would happen if the command would execute, but does not actually perform the command + + .PARAMETER OutputPath + Directory to parsed diagnostict queries to. This will split them based on server, databasename, and query. + + .PARAMETER ExportQueries + Use this switch to export the diagnostic queries to sql files. I + nstead of running the queries, the server will be evaluated to find the appropriate queries to run based on SQL Version. + These sql files will then be created in the OutputDirectory. + + + + .NOTES + Tags: Database, DMV + Author: André Kamman (@AndreKamman), http://clouddba.io + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaDiagnosticQuery + + .EXAMPLE + Invoke-DbaDiagnosticQuery -SqlInstance sql2016 + + Run the selection made by the user on the Sql Server instance specified. + + .EXAMPLE + Invoke-DbaDiagnosticQuery -SqlInstance sql2016 -UseSelectionHelper | Export-DbaDiagnosticQuery -Path C:\temp\gboutput + + Provides a gridview with all the queries to choose from and will run the selection made by the user on the SQL Server instance specified. + Then it will export the results to Export-DbaDiagnosticQuery. + + .Example + # Exporting Queries To SQL Files + - Parse the appropriate diagnostic queries by connecting to server to get version matched queries + - Instead of running the diagnostic queries, export them to SQL files + + # Export All Queries to Disk + Invoke-DbaDiagnosticQuery -sqlinstance localhost -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries for all User Dbs + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries For One Target Database + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" + + # Export Database Specific Queries For One Target Database and One Specific Query + Invoke-DbaDiagnosticQuery -sqlinstance localhost -DatabaseSpecific -DatabaseName 'tempdb' -ExportQueries -outputpath "C:\temp\DiagnosticQueries" -queryname 'Database-scoped Configurations' + + # Choose Queries To Export + Invoke-DbaDiagnosticQuery -sqlinstance localhost -UseSelectionHelper + + This will export with SqlInstance, DatabaseName, and QueryName as appropriate based on query. + + .Example + # Export Queries (not run) as returned object + + [System.Management.Automation.PSObject[]]$results = Invoke-DbaDiagnosticQuery -SqlInstance localhost -whatif + + Parse the appropriate diagnostic queries by connecting to server, and instead of running them, return as [pscustomobject[]] to work with further + + .Example + # Database Specific Handling + $results = Invoke-DbaDiagnosticQuery -SqlInstance $script:instance2 -DatabaseSpecific -queryname 'Database-scoped Configurations' -databasename $database + + Run diagnostic queries targeted at specific database, and only run database level queries against this database. + + #> + + [CmdletBinding(SupportsShouldProcess)] + [outputtype([pscustomobject[]])] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + + [Alias('DatabaseName')] + [object[]]$Database, + + [object[]]$ExcludeDatabase, + + [Alias('Credential')] + [PSCredential]$SqlCredential, + [System.IO.FileInfo]$Path, + [string[]]$QueryName, + [switch]$UseSelectionHelper, + [switch]$InstanceOnly, + [switch]$DatabaseSpecific, + [Switch]$NoQueryTextColumn, + [Switch]$NoPlanColumn, + [Switch]$NoColumnParsing, + + [string]$OutputPath, + [switch]$ExportQueries, + + [switch][Alias('Silent')] + [switch]$EnableException + ) + + begin { + $ProgressId = Get-Random + + function Invoke-DiagnosticQuerySelectionHelper { + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true)] + $ParsedScript + ) + + $ParsedScript | Select-Object QueryNr, QueryName, DBSpecific, Description | Out-GridView -Title "Diagnostic Query Overview" -OutputMode Multiple | Sort-Object QueryNr | Select-Object -ExpandProperty QueryName + + } + + Write-Message -Level Verbose -Message "Interpreting DMV Script Collections" + + $module = Get-Module -Name dbatools + $base = $module.ModuleBase + + if (!$Path) { + $Path = "$base\bin\diagnosticquery" + } + + $scriptversions = @() + $scriptfiles = Get-ChildItem "$Path\SQLServerDiagnosticQueries_*_*.sql" + + if (!$scriptfiles) { + Write-Message -Level Warning -Message "Diagnostic scripts not found in $Path. Using the ones within the module." + + $Path = "$base\bin\diagnosticquery" + + $scriptfiles = Get-ChildItem "$base\bin\diagnosticquery\SQLServerDiagnosticQueries_*_*.sql" + if (!$scriptfiles) { + Stop-Function -Message "Unable to download scripts, do you have an internet connection? $_" -ErrorRecord $_ + return + } + } + + [int[]]$filesort = $null + + foreach ($file in $scriptfiles) { + $filesort += $file.BaseName.Split("_")[2] + } + + $currentdate = $filesort | Sort-Object -Descending | Select-Object -First 1 + + foreach ($file in $scriptfiles) { + if ($file.BaseName.Split("_")[2] -eq $currentdate) { + $parsedscript = Invoke-DbaDiagnosticQueryScriptParser -filename $file.fullname -NoQueryTextColumn:$NoQueryTextColumn -NoPlanColumn:$NoPlanColumn -NoColumnParsing:$NoColumnParsing + + $newscript = [pscustomobject]@{ + Version = $file.Basename.Split("_")[1] + Script = $parsedscript + } + $scriptversions += $newscript + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + $counter = 0 + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + Write-Message -Level Verbose -Message "Collecting diagnostic query data from server: $instance" + + if ($server.VersionMinor -eq 50) { + $version = "2008R2" + } + else { + $version = switch ($server.VersionMajor) { + 9 { "2005" } + 10 { "2008" } + 11 { "2012" } + 12 { "2014" } + 13 { "2016" } + 14 { "2017" } + } + } + + if (!$instanceOnly) { + if (-not $Database) { + $databases = (Get-DbaDatabase -SqlInstance $server -ExcludeAllSystemDb -ExcludeDatabase $ExcludeDatabase).Name + } + else { + $databases = (Get-DbaDatabase -SqlInstance $server -ExcludeAllSystemDb -Database $Database -ExcludeDatabase $ExcludeDatabase).Name + } + } + + $parsedscript = $scriptversions | Where-Object -Property Version -eq $version | Select-Object -ExpandProperty Script + + if ($null -eq $first) { $first = $true } + if ($UseSelectionHelper -and $first) { + $QueryName = Invoke-DiagnosticQuerySelectionHelper $parsedscript + $first = $false + } + #since some database level queries can take longer (such as fragmentation) calculate progress with database specific queries * count of databases to run against into context + $CountOfDatabases = ($databases).Count + + + if ($QueryName.Count -ne 0) { + #if running all queries, then calculate total to run by instance queries count + (db specific count * databases to run each against) + $countDBSpecific = @($parsedscript | Where-Object {$_.QueryName -in $QueryName -and $_.DBSpecific -eq $true}).Count + $countInstanceSpecific = @($parsedscript | Where-Object {$_.QueryName -in $QueryName -and $_.DBSpecific -eq $false}).Count + } + else { + #if narrowing queries to database specific, calculate total to process based on instance queries count + (db specific count * databases to run each against) + $countDBSpecific = @($parsedscript | Where-Object DBSpecific).Count + $countInstanceSpecific = @($parsedscript | Where-Object DBSpecific -eq $false).Count + + } + if (!$instanceonly -and !$DatabaseSpecific -and !$QueryName) { + $scriptcount = $countInstanceSpecific + ($countDBSpecific * $CountOfDatabases ) + } + elseif ($instanceOnly) { + $scriptcount = $countInstanceSpecific + } + elseif ($DatabaseSpecific) { + $scriptcount = $countDBSpecific * $CountOfDatabases + } + elseif ($QueryName.Count -ne 0) { + $scriptcount = $countInstanceSpecific + ($countDBSpecific * $CountOfDatabases ) + + + } + + foreach ($scriptpart in $parsedscript) { + + if (($QueryName.Count -ne 0) -and ($QueryName -notcontains $scriptpart.QueryName)) { continue } + if (!$scriptpart.DBSpecific -and !$DatabaseSpecific) { + if ($ExportQueries) { + $null = New-Item -Path $OutputPath -ItemType Directory -Force + $FileName = Remove-InvalidFileNameChars ('{0}.sql' -f $Scriptpart.QueryName) + $FullName = Join-Path $OutputPath $FileName + Write-Message -Level Verbose -Message "Creating file: $FullName" + $scriptPart.Text | out-file -FilePath $FullName -Encoding UTF8 -force + continue + } + + if ($PSCmdlet.ShouldProcess($instance, $scriptpart.QueryName)) { + + if (-not $EnableException) { + $Counter++ + Write-Progress -Id $ProgressId -ParentId 0 -Activity "Collecting diagnostic query data from $instance" -Status "Processing $counter of $scriptcount" -CurrentOperation $scriptpart.QueryName -PercentComplete (($counter / $scriptcount) * 100) + } + + try { + $result = $server.Query($scriptpart.Text) + Write-Message -Level Verbose -Message "Processed $($scriptpart.QueryName) on $instance" + if (!$result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "Empty Result for this Query" + Result = $null + } + Write-Message -Level Verbose -Message ("Empty result for Query {0} - {1} - {2}" -f $scriptpart.QueryNr, $scriptpart.QueryName, $scriptpart.Description) + } + } + catch { + Write-Message -Level Verbose -Message ('Some error has occured on Server: {0} - Script: {1}, result unavailable' -f $instance, $scriptpart.QueryName) -Target $instance -ErrorRecord $_ + } + if ($result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = $null + Result = Select-DefaultView -InputObject $result -Property * + } + + } + } + else { + # if running WhatIf, then return the queries that would be run as an object, not just whatif output + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "WhatIf - Bypassed Execution" + Result = $null + } + } + + } + elseif ($scriptpart.DBSpecific -and !$instanceOnly) { + + foreach ($currentdb in $databases) { + if ($ExportQueries) { + $null = New-Item -Path $OutputPath -ItemType Directory -Force + $FileName = Remove-InvalidFileNameChars ('{0}-{1}-{2}.sql' -f $server.DomainInstanceName, $currentDb, $Scriptpart.QueryName) + $FullName = Join-Path $OutputPath $FileName + Write-Message -Level Verbose -Message "Creating file: $FullName" + $scriptPart.Text | out-file -FilePath $FullName -encoding UTF8 -force + continue + } + + + if ($PSCmdlet.ShouldProcess(('{0} ({1})' -f $instance, $currentDb), $scriptpart.QueryName)) { + + if (-not $EnableException) { + $Counter++ + Write-Progress -Id $ProgressId -ParentId 0 -Activity "Collecting diagnostic query data from $($currentDb) on $instance" -Status ('Processing {0} of {1}' -f $counter, $scriptcount) -CurrentOperation $scriptpart.QueryName -PercentComplete (($Counter / $scriptcount) * 100) + } + + Write-Message -Level Verbose -Message "Collecting diagnostic query data from $($currentDb) for $($scriptpart.QueryName) on $instance" + try { + $result = $server.Query($scriptpart.Text, $currentDb) + if (!$result) { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "Empty Result for this Query" + Result = $null + } + Write-Message -Level Verbose -Message ("Empty result for Query {0} - {1} - {2}" -f $scriptpart.QueryNr, $scriptpart.QueryName, $scriptpart.Description) -Target $scriptpart -ErrorRecord $_ + } + } + catch { + Write-Message -Level Verbose -Message ('Some error has occured on Server: {0} - Script: {1} - Database: {2}, result will not be saved' -f $instance, $scriptpart.QueryName, $currentDb) -Target $currentdb -ErrorRecord $_ + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $currentDb + Notes = $null + Result = Select-DefaultView -InputObject $result -Property * + } + } + else { + # if running WhatIf, then return the queries that would be run as an object, not just whatif output + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Number = $scriptpart.QueryNr + Name = $scriptpart.QueryName + Description = $scriptpart.Description + DatabaseSpecific = $scriptpart.DBSpecific + Database = $null + Notes = "WhatIf - Bypassed Execution" + Result = $null + } + } + } + } + } + } + } + end { + Write-Progress -Id $ProgressId -Activity 'Invoke-DbaDiagnosticQuery' -Completed + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaLogShipping.ps1 b/functions/Invoke-DbaLogShipping.ps1 new file mode 100644 index 000000000000..9a3780f7b432 --- /dev/null +++ b/functions/Invoke-DbaLogShipping.ps1 @@ -0,0 +1,1794 @@ +function Invoke-DbaLogShipping { + <# + .SYNOPSIS + Invoke-DbaLogShipping sets up log shipping for one or more databases + + .DESCRIPTION + Invoke-DbaLogShipping helps to easily set up log shipping for one or more databases. + + This function will make a lot of decisions for you assuming you want default values like a daily interval for the schedules with a 15 minute interval on the day. + There are some settings that cannot be made by the function and they need to be prepared before the function is executed. + + The following settings need to be made before log shipping can be initiated: + - Backup destination (the folder and the privileges) + - Copy destination (the folder and the privileges) + + * Privileges + Make sure your agent service on both the primary and the secondary instance is an Active Directory account. + Also have the credentials ready to set the folder permissions + + ** Network share + The backup destination needs to be shared and have the share privileges of FULL CONTROL to Everyone. + + ** NTFS permissions + The backup destination must have at least read/write permissions for the primary instance agent account. + The backup destination must have at least read permissions for the secondary instance agent account. + The copy destination must have at least read/write permission for the secondary instance agent acount. + + .PARAMETER SourceSqlInstance + Source SQL Server instance which contains the databases to be log shipped. + You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER DestinationSqlInstance + Destination SQL Server instance which contains the databases to be log shipped. + You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER SourceCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Database to set up log shipping for. + + .PARAMETER BackupNetworkPath + The backup unc path to place the backup files. This is the root directory. + A directory with the name of the database will be created in this path. + + .PARAMETER BackupLocalPath + If the backup path is locally for the source server you can also set this value. + + .PARAMETER BackupJob + Name of the backup that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the backup job name. + The default is "LSBackup_[databasename]" + + .PARAMETER BackupRetention + The backup retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER BackupSchedule + Name of the backup schedule created for the backup job. + The parameter works as a prefix where the name of the database will be added to the backup job schedule name. + Default is "LSBackupSchedule_[databasename]" + + .PARAMETER BackupScheduleDisabled + Parameter to set the backup schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER BackupScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER BackupScheduleFrequencyInterval + The number of type periods to occur between each execution of the backup job. + + .PARAMETER BackupScheduleFrequencySubdayType + Specifies the units for the sub-day FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER BackupScheduleFrequencySubdayInterval + The number of sub-day type periods to occur between each execution of the backup job. + + .PARAMETER BackupScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER BackupScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER BackupScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER BackupScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER BackupScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER BackupScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER BackupThreshold + Is the length of time, in minutes, after the last backup before a threshold alert error is raised. + The default is 60. + + .PARAMETER CompressBackup + Do the backups need to be compressed. By default the backups are not compressed. + + .PARAMETER CopyDestinationFolder + The path to copy the transaction log backup files to. This is the root directory. + A directory with the name of the database will be created in this path. + + .PARAMETER CopyJob + Name of the copy job that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the copy job name. + The default is "LSBackup_[databasename]" + + .PARAMETER CopyRetention + The copy retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER CopySchedule + Name of the backup schedule created for the copy job. + The parameter works as a prefix where the name of the database will be added to the copy job schedule name. + Default is "LSCopy_[DestinationServerName]_[DatabaseName]" + + .PARAMETER CopyScheduleDisabled + Parameter to set the copy schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER CopyScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER CopyScheduleFrequencyInterval + The number of type periods to occur between each execution of the copy job. + + .PARAMETER CopyScheduleFrequencySubdayType + Specifies the units for the subday FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER CopyScheduleFrequencySubdayInterval + The number of subday type periods to occur between each execution of the copy job. + + .PARAMETER CopyScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER CopyScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER CopyScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER CopyScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER CopyScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER CopyScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER DisconnectUsers + If this parameter is set in combinations of standby the users will be disconnected during restore. + + .PARAMETER FullBackupPath + Path to an existing full backup. Use this when an existing backup needs to used to initialize the database on the secondary instance. + + .PARAMETER GenerateFullBackup + If the database is not initialized on the secondary instance it can be done by creating a new full backup and + restore it for you. + + .PARAMETER HistoryRetention + Is the length of time in minutes in which the history is retained. + The default value is 14420 + + .PARAMETER NoRecovery + If this parameter is set the database will be in recovery mode. The database will not be readable. + This setting is default. + + .PARAMETER NoInitialization + If this parameter is set the secondary database will not be initialized. + The database needs to be on the secondary instance in recovery mode. + + .PARAMETER PrimaryMonitorServer + Is the name of the monitor server for the primary server. + The default is the name of the primary sql server. + + .PARAMETER PrimaryMonitorCredential + Allows you to login to enter a secure credential. Only needs to be used when the PrimaryMonitorServerSecurityMode is 0 or "sqlserver" + To use: $scred = Get-Credential, then pass $scred object to the -PrimaryMonitorCredential parameter. + + .PARAMETER PrimaryMonitorServerSecurityMode + The security mode used to connect to the monitor server for the primary server. Allowed values are 0, "sqlserver", 1, "windows" + The default is 1 or Windows. + + .PARAMETER PrimaryThresholdAlertEnabled + Enables the Threshold alert for the primary database + + .PARAMETER RestoreDataFolder + Folder to be used to restore the database data files. Only used when parameter GenerateFullBackup or UseExistingFullBackup are set. + If the parameter is not set the default data folder of the secondary instance will be used including the name of the database. + If the folder is set but doesn't exist the default data folder of the secondary instance will be used including the name of the database. + + .PARAMETER RestoreLogFolder + Folder to be used to restore the database log files. Only used when parameter GenerateFullBackup or UseExistingFullBackup are set. + If the parameter is not set the default transaction log folder of the secondary instance will be used. + If the folder is set but doesn't exist the default transaction log folder of the secondary instance will be used. + + .PARAMETER RestoreDelay + In case a delay needs to be set for the restore. + The default is 0. + + .PARAMETER RestoreAlertThreshold + The amount of minutes after which an alert will be raised is no restore has taken place. + The default is 45 minutes. + + .PARAMETER RestoreJob + Name of the restore job that will be created in the SQL Server agent. + The parameter works as a prefix where the name of the database will be added to the restore job name. + The default is "LSRestore_[databasename]" + + .PARAMETER RestoreRetention + The backup retention period in minutes. Default is 4320 / 72 hours + + .PARAMETER RestoreSchedule + Name of the backup schedule created for the restore job. + The parameter works as a prefix where the name of the database will be added to the restore job schedule name. + Default is "LSRestore_[DestinationServerName]_[DatabaseName]" + + .PARAMETER RestoreScheduleDisabled + Parameter to set the restore schedule to disabled upon creation. + By default the schedule is enabled. + + .PARAMETER RestoreScheduleFrequencyType + A value indicating when a job is to be executed. + Allowed values are "Daily", "AgentStart", "IdleComputer" + + .PARAMETER RestoreScheduleFrequencyInterval + The number of type periods to occur between each execution of the restore job. + + .PARAMETER RestoreScheduleFrequencySubdayType + Specifies the units for the subday FrequencyInterval. + Allowed values are "Time", "Seconds", "Minutes", "Hours" + + .PARAMETER RestoreScheduleFrequencySubdayInterval + The number of subday type periods to occur between each execution of the restore job. + + .PARAMETER RestoreScheduleFrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + .PARAMETER RestoreScheduleFrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + + .PARAMETER RestoreScheduleStartDate + The date on which execution of a job can begin. + + .PARAMETER RestoreScheduleEndDate + The date on which execution of a job can stop. + + .PARAMETER RestoreScheduleStartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER RestoreScheduleEndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + .PARAMETER RestoreThreshold + The number of minutes allowed to elapse between restore operations before an alert is generated. + The default value = 0 + + .PARAMETER SecondaryDatabasePrefix + The secondary database can be renamed to include a prefix. + + .PARAMETER SecondaryDatabaseSuffix + The secondary database can be renamed to include a suffix. + + .PARAMETER SecondaryMonitorServer + Is the name of the monitor server for the secondary server. + The default is the name of the secondary sql server. + + .PARAMETER SecondaryMonitorCredential + Allows you to login to enter a secure credential. Only needs to be used when the SecondaryMonitorServerSecurityMode is 0 or "sqlserver" + To use: $scred = Get-Credential, then pass $scred object to the -SecondaryMonitorCredential parameter. + + .PARAMETER SecondaryMonitorServerSecurityMode + The security mode used to connect to the monitor server for the secondary server. Allowed values are 0, "sqlserver", 1, "windows" + The default is 1 or Windows. + + .PARAMETER SecondaryThresholdAlertEnabled + Enables the Threshold alert for the secondary database + + .PARAMETER Standby + If this parameter is set the database will be set to standby mode making the database readable. + If not set the database will be in recovery mode. + + .PARAMETER StandbyDirectory + Directory to place the standby file(s) in + + .PARAMETER UseExistingFullBackup + If the database is not initialized on the secondary instance it can be done by selecting an existing full backup + and restore it for you. + + .PARAMETER UseBackupFolder + This enables the user to specify a specific backup folder containing one or more backup files to initialize the database on the secondary instance. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + Use this switch to disable any kind of verbose messages + + .PARAMETER Force + The force parameter will ignore some errors in the parameters and assume defaults. + It will also remove the any present schedules with the same name for the specific job. + + .NOTES + Tags: LogShipping + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaLogShipping + + .EXAMPLE + $params = @{ + SourceSqlInstance = 'sql1' + DestinationSqlInstance = 'sql2' + Database = 'db1' + BackupNetworkPath= '\\sql1\logshipping' + BackupLocalPath= 'D:\Data\logshipping' + BackupScheduleFrequencyType = 'daily' + BackupScheduleFrequencyInterval = 1 + CompressBackup = $true + CopyScheduleFrequencyType = 'daily' + CopyScheduleFrequencyInterval = 1 + GenerateFullBackup = $true + RestoreScheduleFrequencyType = 'daily' + RestoreScheduleFrequencyInterval = 1 + SecondaryDatabaseSuffix = 'DR' + CopyDestinationFolder = '\\sql2\logshippingdest' + Force = $true + } + + Invoke-DbaLogShipping @params + + Sets up log shipping for database "db1" with the backup path to a network share allowing local backups. + It creates daily schedules for the backup, copy and restore job with all the defaults to be executed every 15 minutes daily. + The secondary database will be called "db1_LS". + + .EXAMPLE + $params = @{ + SourceSqlInstance = 'sql1' + DestinationSqlInstance = 'sql2' + Database = 'db1' + BackupNetworkPath= '\\sql1\logshipping' + GenerateFullBackup = $true + Force = $true + } + + Invoke-DbaLogShipping @params + + Sets up log shipping with all defaults except that a backup file is generated. + The script will show a message that the copy destination has not been supplied and asks if you want to use the default which would be the backup directory of the secondary server with the folder "logshipping" i.e. "D:\SQLBackup\Logshiping". + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("SourceServerInstance", "SourceSqlServerSqlServer", "Source")] + [object]$SourceSqlInstance, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("DestinationServerInstance", "DestinationSqlServer", "Destination")] + [object]$DestinationSqlInstance, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SourceSqlCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SourceCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $DestinationSqlCredential, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $DestinationCredential, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Database, + + [parameter(Mandatory = $true)] + [string]$BackupNetworkPath, + + [parameter(Mandatory = $false)] + [string]$BackupLocalPath, + + [parameter(Mandatory = $false)] + [string]$BackupJob, + + [parameter(Mandatory = $false)] + [int]$BackupRetention, + + [parameter(Mandatory = $false)] + [string]$BackupSchedule, + + [parameter(Mandatory = $false)] + [switch]$BackupScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$BackupScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$BackupScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$BackupScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$BackupScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$BackupScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$BackupScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$BackupScheduleEndTime, + + [parameter(Mandatory = $false)] + [int]$BackupThreshold, + + [parameter(Mandatory = $false)] + [switch]$CompressBackup, + + [parameter(Mandatory = $false)] + [string]$CopyDestinationFolder, + + [parameter(Mandatory = $false)] + [string]$CopyJob, + + [parameter(Mandatory = $false)] + [int]$CopyRetention, + + [parameter(Mandatory = $false)] + [string]$CopySchedule, + + [parameter(Mandatory = $false)] + [switch]$CopyScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$CopyScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$CopyScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$CopyScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$CopyScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$CopyScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$CopyScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$CopyScheduleEndTime, + + [parameter(Mandatory = $false)] + [switch]$DisconnectUsers, + + [parameter(Mandatory = $false)] + [string]$FullBackupPath, + + [parameter(Mandatory = $false)] + [switch]$GenerateFullBackup, + + [parameter(Mandatory = $false)] + [int]$HistoryRetention, + + [parameter(Mandatory = $false)] + [switch]$NoRecovery, + + [parameter(Mandatory = $false)] + [switch]$NoInitialization, + + [Parameter(Mandatory = $false)] + [string]$PrimaryMonitorServer, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $PrimaryMonitorCredential, + + [Parameter(Mandatory = $false)] + [ValidateSet(0, "sqlserver", 1, "windows")] + [object]$PrimaryMonitorServerSecurityMode, + + [Parameter(Mandatory = $false)] + [switch]$PrimaryThresholdAlertEnabled, + + [parameter(Mandatory = $false)] + [string]$RestoreDataFolder, + + [parameter(Mandatory = $false)] + [string]$RestoreLogFolder, + + [parameter(Mandatory = $false)] + [int]$RestoreDelay, + + [parameter(Mandatory = $false)] + [int]$RestoreAlertThreshold, + + [parameter(Mandatory = $false)] + [string]$RestoreJob, + + [parameter(Mandatory = $false)] + [int]$RestoreRetention, + + [parameter(Mandatory = $false)] + [string]$RestoreSchedule, + + [parameter(Mandatory = $false)] + [switch]$RestoreScheduleDisabled, + + [parameter(Mandatory = $false)] + [ValidateSet("Daily", "Weekly", "AgentStart", "IdleComputer")] + [object]$RestoreScheduleFrequencyType, + + [parameter(Mandatory = $false)] + [object[]]$RestoreScheduleFrequencyInterval, + + [parameter(Mandatory = $false)] + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$RestoreScheduleFrequencySubdayType, + + [parameter(Mandatory = $false)] + [int]$RestoreScheduleFrequencySubdayInterval, + + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$RestoreScheduleFrequencyRelativeInterval, + + [Parameter(Mandatory = $false)] + [int]$RestoreScheduleFrequencyRecurrenceFactor, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleStartDate, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleEndDate, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleStartTime, + + [parameter(Mandatory = $false)] + [string]$RestoreScheduleEndTime, + + [parameter(Mandatory = $false)] + [int]$RestoreThreshold, + + [parameter(Mandatory = $false)] + [string]$SecondaryDatabasePrefix, + + [parameter(Mandatory = $false)] + [string]$SecondaryDatabaseSuffix, + + [Parameter(Mandatory = $false)] + [string]$SecondaryMonitorServer, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + $SecondaryMonitorCredential, + + [Parameter(Mandatory = $false)] + [ValidateSet(0, "sqlserver", 1, "windows")] + [object]$SecondaryMonitorServerSecurityMode, + + [Parameter(Mandatory = $false)] + [switch]$SecondaryThresholdAlertEnabled, + + [parameter(Mandatory = $false)] + [switch]$Standby, + + [parameter(Mandatory = $false)] + [string]$StandbyDirectory, + + [parameter(Mandatory = $false)] + [switch]$UseExistingFullBackup, + + [parameter(Mandatory = $false)] + [string]$UseBackupFolder, + + [switch]$Force, + + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Write-Message -Message "Started log shipping for $SourceSqlInstance to $DestinationSqlInstance" -Level Output + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to source Sql Server $SourceSqlInstance.." -Level Output + try { + $SourceServer = Connect-SqlInstance -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $SourceSqlInstance" -ErrorRecord $_ -Target $SourceSqlInstance + return + } + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to destination Sql Server $DestinationSqlInstance.." -Level Output + try { + $DestinationServer = Connect-SqlInstance -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Could not connect to Sql Server instance $DestinationSqlInstance" -ErrorRecord $_ -Target $DestinationSqlInstance + return + } + + # Check the instance if it is a named instance + $SourceServerName, $SourceInstanceName = $SourceSqlInstance.Split("\") + $DestinationServerName, $DestinationInstanceName = $DestinationSqlInstance.Split("\") + + if ($SourceInstanceName -eq $null) { + $SourceInstanceName = "MSSQLSERVER" + } + + if ($DestinationInstanceName -eq $null) { + $DestinationInstanceName = "MSSQLSERVER" + } + + $IsSourceLocal = $false + $IsDestinationLocal = $false + + # Check if it's local or remote + if ($SourceServerName -in ".", "localhost", $env:ServerName, "127.0.0.1") { + $IsSourceLocal = $true + } + if ($DestinationServerName -in ".", "localhost", $env:ServerName, "127.0.0.1") { + $IsDestinationLocal = $true + } + + # Set up regex strings for several checks + $RegexDate = '(?:`"/\\|?*]+)+$' + + # Check the instance names and the database settings + if (($SourceSqlInstance -eq $DestinationSqlInstance) -and (-not $SecondaryDatabasePrefix -or $SecondaryDatabaseSuffix)) { + Stop-Function -Message "The destination database is the same as the source`nPlease enter a prefix or suffix using -SecondaryDatabasePrefix or -SecondaryDatabaseSuffix." -Target $SourceSqlInstance + return + } + + # Check the connection timeout + if ($SourceServer.ConnectionContext.StatementTimeout -ne 0) { + $SourceServer.ConnectionContext.StatementTimeout = 0 + Write-Message -Message "Connection timeout of $SourceServer is set to 0" -Level Verbose + } + + if ($DestinationServer.ConnectionContext.StatementTimeout -ne 0) { + $DestinationServer.ConnectionContext.StatementTimeout = 0 + Write-Message -Message "Connection timeout of $DestinationServer is set to 0" -Level Verbose + } + + # Check the backup network path + Write-Message -Message "Testing backup network path $BackupNetworkPath" -Level Verbose + if ((Test-DbaSqlPath -Path $BackupNetworkPath -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + Stop-Function -Message "Backup network path $BackupNetworkPath is not valid or can't be reached." -Target $SourceSqlInstance + return + } + elseif ($BackupNetworkPath -notmatch $RegexUnc) { + Stop-Function -Message "Backup network path $BackupNetworkPath has to be in the form of \\server\share." -Target $SourceSqlInstance + return + } + + # Check the copy destination + if (-not $CopyDestinationFolder) { + # Make a default copy destination by retrieving the backup folder and adding a directory + $CopyDestinationFolder = "$($DestinationServer.Settings.BackupDirectory)\Logshipping" + + # Check to see if the path already exists + Write-Message -Message "Testing copy destination path $CopyDestinationFolder" -Level Verbose + if (Test-DbaSqlPath -Path $CopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) { + Write-Message -Message "Copy destination $CopyDestinationFolder already exists" -Level Verbose + } + else { + # Check if force is being used + if (-not $Force) { + # Set up the confirm part + $message = "The copy destination is missing. Do you want to use the default $($CopyDestinationFolder)?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Try to create the new directory + try { + # If the destination server is remote and the credential is set + if (-not $IsDestinationLocal -and $DestinationCredential) { + Invoke-Command2 -ComputerName $DestinationServerName -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + # If the server is local and the credential is set + elseif ($DestinationCredential) { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + # If the server is local and the credential is not set + else { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item -Path $CopyDestinationFolder -Force:$Force -ItemType Directory | Out-Null + } + Write-Message -Message "Copy destination $CopyDestinationFolder created." -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong creating the copy destination folder $CopyDestinationFolder. `n$_" -Target $DestinationSqlInstance -ErrorRecord $_ + return + } + } + 1 { + Stop-Function -Message "Copy destination is a mandatory parameter. Please make sure the value is entered." -Target $DestinationSqlInstance + return + } + } # switch + } # if not force + else { + # Try to create the copy destination on the local server + try { + Write-Message -Message "Creating copy destination folder $CopyDestinationFolder" -Level Verbose + New-Item $CopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + Write-Message -Message "Copy destination $CopyDestinationFolder created." -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong creating the copy destination folder $CopyDestinationFolder. `n$_" -Target $DestinationSqlInstance -ErrorRecord $_ + return + } + } # else not force + } # if test path copy destination + } # if not copy destination + + Write-Message -Message "Testing copy destination path $CopyDestinationFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $CopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message "Copy destination folder $CopyDestinationFolder is not valid or can't be reached." -Target $DestinationSqlInstance + return + } + elseif ($CopyDestinationFolder.StartsWith("\\") -and $CopyDestinationFolder -notmatch $RegexUnc) { + Stop-Function -Message "Copy destination folder $CopyDestinationFolder has to be in the form of \\server\share." -Target $DestinationSqlInstance + return + } + + # Check the backup compression + if ($SourceServer.Version.Major -gt 9) { + if ($CompressBackup) { + Write-Message -Message "Setting backup compression to 1." -Level Verbose + [bool]$BackupCompression = 1 + } + else { + $backupServerSetting = (Get-DbaSpConfigure -SqlInstance $SourceSqlInstance -ConfigName DefaultBackupCompression).ConfiguredValue + Write-Message -Message "Setting backup compression to default server setting $backupServerSetting." -Level Verbose + [bool]$BackupCompression = $backupServerSetting + } + } + else { + Write-Message -Message "Source server $SourceServer does not support backup compression" -Level Verbose + } + + # Check the database parameter + if ($Database) { + foreach ($db in $Database) { + if ($db -notin $SourceServer.Databases.Name) { + Stop-Function -Message "Database $db cannot be found on instance $SourceSqlInstance" -Target $SourceSqlInstance + } + + $DatabaseCollection = $SourceServer.Databases | Where-Object { $_.Name -in $Database } + } + } + else { + Stop-Function -Message "Please supply a database to set up log shipping for" -Target $SourceSqlInstance -Continue + } + + # Set the database mode + if ($Standby) { + $DatabaseStatus = 1 + Write-Message -Message "Destination database status set to STANDBY" -Level Verbose + } + else { + $DatabaseStatus = 0 + Write-Message -Message "Destination database status set to NO RECOVERY" -Level Verbose + } + + # Setting defaults + if (-not $BackupRetention) { + $BackupRetention = 4320 + Write-Message -Message "Backup retention set to $BackupRetention" -Level Verbose + } + if (-not $BackupThreshold) { + $BackupThreshold = 60 + Write-Message -Message "Backup Threshold set to $BackupThreshold" -Level Verbose + } + if (-not $CopyRetention) { + $CopyRetention = 4320 + Write-Message -Message "Copy retention set to $CopyRetention" -Level Verbose + } + if (-not $HistoryRetention) { + $HistoryRetention = 14420 + Write-Message -Message "History retention set to $HistoryRetention" -Level Verbose + } + if (-not $RestoreAlertThreshold) { + $RestoreAlertThreshold = 45 + Write-Message -Message "Restore alert Threshold set to $RestoreAlertThreshold" -Level Verbose + } + if (-not $RestoreDelay) { + $RestoreDelay = 0 + Write-Message -Message "Restore delay set to $RestoreDelay" -Level Verbose + } + if (-not $RestoreRetention) { + $RestoreRetention = 4320 + Write-Message -Message "Restore retention set to $RestoreRetention" -Level Verbose + } + if (-not $RestoreThreshold) { + $RestoreThreshold = 0 + Write-Message -Message "Restore Threshold set to $RestoreThreshold" -Level Verbose + } + if (-not $PrimaryMonitorServerSecurityMode) { + $PrimaryMonitorServerSecurityMode = 1 + Write-Message -Message "Primary monitor server security mode set to $PrimaryMonitorServerSecurityMode" -Level Verbose + } + if (-not $SecondaryMonitorServerSecurityMode) { + $SecondaryMonitorServerSecurityMode = 1 + Write-Message -Message "Secondary monitor server security mode set to $SecondaryMonitorServerSecurityMode" -Level Verbose + } + if (-not $BackupScheduleFrequencyType) { + $BackupScheduleFrequencyType = "Daily" + Write-Message -Message "Backup frequency type set to $BackupScheduleFrequencyType" -Level Verbose + } + if (-not $BackupScheduleFrequencyInterval) { + $BackupScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Backup frequency interval set to $BackupScheduleFrequencyInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencySubdayType) { + $BackupScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Backup frequency subday type set to $BackupScheduleFrequencySubdayType" -Level Verbose + } + if (-not $BackupScheduleFrequencySubdayInterval) { + $BackupScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Backup frequency subday interval set to $BackupScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencyRelativeInterval) { + $BackupScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Backup frequency relative interval set to $BackupScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $BackupScheduleFrequencyRecurrenceFactor) { + $BackupScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Backup frequency recurrence factor set to $BackupScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not $CopyScheduleFrequencyType) { + $CopyScheduleFrequencyType = "Daily" + Write-Message -Message "Copy frequency type set to $CopyScheduleFrequencyType" -Level Verbose + } + if (-not $CopyScheduleFrequencyInterval) { + $CopyScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Copy frequency interval set to $CopyScheduleFrequencyInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencySubdayType) { + $CopyScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Copy frequency subday type set to $CopyScheduleFrequencySubdayType" -Level Verbose + } + if (-not $CopyScheduleFrequencySubdayInterval) { + $CopyScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Copy frequency subday interval set to $CopyScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencyRelativeInterval) { + $CopyScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Copy frequency relative interval set to $CopyScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $CopyScheduleFrequencyRecurrenceFactor) { + $CopyScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Copy frequency recurrence factor set to $CopyScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not $RestoreScheduleFrequencyType) { + $RestoreScheduleFrequencyType = "Daily" + Write-Message -Message "Restore frequency type set to $RestoreScheduleFrequencyType" -Level Verbose + } + if (-not $RestoreScheduleFrequencyInterval) { + $RestoreScheduleFrequencyInterval = "EveryDay" + Write-Message -Message "Restore frequency interval set to $RestoreScheduleFrequencyInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencySubdayType) { + $RestoreScheduleFrequencySubdayType = "Minutes" + Write-Message -Message "Restore frequency subday type set to $RestoreScheduleFrequencySubdayType" -Level Verbose + } + if (-not $RestoreScheduleFrequencySubdayInterval) { + $RestoreScheduleFrequencySubdayInterval = 15 + Write-Message -Message "Restore frequency subday interval set to $RestoreScheduleFrequencySubdayInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencyRelativeInterval) { + $RestoreScheduleFrequencyRelativeInterval = "Unused" + Write-Message -Message "Restore frequency relative interval set to $RestoreScheduleFrequencyRelativeInterval" -Level Verbose + } + if (-not $RestoreScheduleFrequencyRecurrenceFactor) { + $RestoreScheduleFrequencyRecurrenceFactor = 0 + Write-Message -Message "Restore frequency recurrence factor set to $RestoreScheduleFrequencyRecurrenceFactor" -Level Verbose + } + if (-not ($SecondaryDatabasePrefix -or $SecondaryDatabaseSuffix) -and ($SourceServer.Name -eq $DestinationServer.Name) -and ($SourceServer.InstanceName -eq $DestinationServer.InstanceName)) { + if ($Force) { + $SecondaryDatabaseSuffix = "_LS" + } + else { + Stop-Function -Message "Destination database is the same as source database.`nPlease check the secondary server, database prefix or suffix or use -Force to set the secondary databse using a suffix." -Target $SourceSqlInstance + return + } + } + + # Checking for contradicting variables + if ($NoInitialization -and ($GenerateFullBackup -or $UseExistingFullBackup)) { + Stop-Function -Message "Cannot use -NoInitialization with -GenerateFullBackup or -UseExistingFullBackup" -Target $DestinationSqlInstance + return + } + + if ($UseBackupFolder -and ($GenerateFullBackup -or $NoInitialization -or $UseExistingFullBackup)) { + Stop-Function -Message "Cannot use -UseBackupFolder with -GenerateFullBackup, -NoInitialization or -UseExistingFullBackup" -Target $DestinationSqlInstance + return + } + + # Check the subday interval + if (($BackupScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($BackupScheduleFrequencySubdayInterval -ge 1 -or $BackupScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Backup subday interval $BackupScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $SourceSqlInstance + return + } + elseif (($BackupScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($BackupScheduleFrequencySubdayInterval -ge 1 -and $BackupScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Backup Subday interval $BackupScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $SourceSqlInstance + return + } + + # Check the subday interval + if (($CopyScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($CopyScheduleFrequencySubdayInterval -ge 1 -or $CopyScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Copy subday interval $CopyScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $DestinationSqlInstance + return + } + elseif (($CopyScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($CopyScheduleFrequencySubdayInterval -ge 1 -and $CopyScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Copy subday interval $CopyScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours'" -Target $DestinationSqlInstance + return + } + + # Check the subday interval + if (($RestoreScheduleFrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($RestoreScheduleFrequencySubdayInterval -ge 1 -or $RestoreScheduleFrequencySubdayInterval -le 59))) { + Stop-Function -Message "Restore subday interval $RestoreScheduleFrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $DestinationSqlInstance + return + } + elseif (($RestoreScheduleFrequencySubdayType -in 8, "Hours") -and (-not ($RestoreScheduleFrequencySubdayInterval -ge 1 -and $RestoreScheduleFrequencySubdayInterval -le 23))) { + Stop-Function -Message "Restore subday interval $RestoreScheduleFrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $DestinationSqlInstance + return + } + + # Check the backup start date + if (-not $BackupScheduleStartDate) { + $BackupScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Backup start date set to $BackupScheduleStartDate" -Level Verbose + } + else { + if ($BackupScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Backup start date $BackupScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the back start time + if (-not $BackupScheduleStartTime) { + $BackupScheduleStartTime = '000000' + Write-Message -Message "Backup start time set to $BackupScheduleStartTime" -Level Verbose + } + elseif ($BackupScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Backup start time $BackupScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the back end time + if (-not $BackupScheduleEndTime) { + $BackupScheduleEndTime = '235959' + Write-Message -Message "Backup end time set to $BackupScheduleEndTime" -Level Verbose + } + elseif ($BackupScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Backup end time $BackupScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the backup end date + if (-not $BackupScheduleEndDate) { + $BackupScheduleEndDate = '99991231' + } + elseif ($BackupScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Backup end date $BackupScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the copy start date + if (-not $CopyScheduleStartDate) { + $CopyScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Copy start date set to $CopyScheduleStartDate" -Level Verbose + } + else { + if ($CopyScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Copy start date $CopyScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the copy end date + if (-not $CopyScheduleEndDate) { + $CopyScheduleEndDate = '99991231' + } + elseif ($CopyScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Copy end date $CopyScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the copy start time + if (-not $CopyScheduleStartTime) { + $CopyScheduleStartTime = '000000' + Write-Message -Message "Copy start time set to $CopyScheduleStartTime" -Level Verbose + } + elseif ($CopyScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Copy start time $CopyScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the copy end time + if (-not $CopyScheduleEndTime) { + $CopyScheduleEndTime = '235959' + Write-Message -Message "Copy end time set to $CopyScheduleEndTime" -Level Verbose + } + elseif ($CopyScheduleEndTime -notmatch $RegexTime) { + Stop-Function -Message "Copy end time $CopyScheduleEndTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the restore start date + if (-not $RestoreScheduleStartDate) { + $RestoreScheduleStartDate = (Get-Date -format "yyyyMMdd") + Write-Message -Message "Restore start date set to $RestoreScheduleStartDate" -Level Verbose + } + else { + if ($RestoreScheduleStartDate -notmatch $RegexDate) { + Stop-Function -Message "Restore start date $RestoreScheduleStartDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + } + + # Check the restore end date + if (-not $RestoreScheduleEndDate) { + $RestoreScheduleEndDate = '99991231' + } + elseif ($RestoreScheduleEndDate -notmatch $RegexDate) { + Stop-Function -Message "Restore end date $RestoreScheduleEndDate needs to be a valid date with format yyyyMMdd" -Target $SourceSqlInstance + return + } + + # Check the restore start time + if (-not $RestoreScheduleStartTime) { + $RestoreScheduleStartTime = '000000' + Write-Message -Message "Restore start time set to $RestoreScheduleStartTime" -Level Verbose + } + elseif ($RestoreScheduleStartTime -notmatch $RegexTime) { + Stop-Function -Message "Restore start time $RestoreScheduleStartTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check the restore end time + if (-not $RestoreScheduleEndTime) { + $RestoreScheduleEndTime = '235959' + Write-Message -Message "Restore end time set to $RestoreScheduleEndTime" -Level Verbose + } + elseif ($RestoreScheduleEndTime -notmatch $RegexTime) { + Stop-Function -Message "Restore end time $RestoreScheduleEndTime needs to match between '000000' and '235959'" -Target $SourceSqlInstance + return + } + + # Check if standby is being used + if ($Standby) { + + # Check the stand-by directory + if ($StandbyDirectory) { + # Check if the path is reachable for the destination server + if ((Test-DbaSqlPath -Path $StandbyDirectory -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message "The directory $StandbyDirectory cannot be reached by the destination instance. Please check the permission and credentials." -Target $DestinationSqlInstance + return + } + } + elseif (-not $StandbyDirectory -and $Force) { + $StandbyDirectory = $DestinationSqlInstance.BackupDirectory + Write-Message -Message "Stand-by directory was not set. Setting it to $StandbyDirectory" -Level Verbose + } + else { + Stop-Function -Message "Please set the parameter -StandbyDirectory when using -Standby" -Target $SourceSqlInstance + return + } + } + } # begin + + process { + + if (Test-FunctionInterrupt) { return } + + # Loop through each of the databases + foreach ($db in $DatabaseCollection) { + + # Check the status of the database + if ($db.RecoveryModel -ne 'Full') { + Stop-Function -Message "Database $db is not in FULL recovery mode" -Target $SourceSqlInstance -Continue + } + + # Set the intital destination database + $SecondaryDatabase = $db.Name + + # Set the database prefix + if ($SecondaryDatabasePrefix) { + $SecondaryDatabase = "$SecondaryDatabasePrefix$($db.Name)" + } + + # Set the database suffix + if ($SecondaryDatabaseSuffix) { + $SecondaryDatabase += $SecondaryDatabaseSuffix + } + + # Check is the database is already initialized an check if the database exists on the secondary instance + if ($NoInitialization -and ($DestinationServer.Databases.Name -notcontains $SecondaryDatabase)) { + Stop-Function -Message "Database $SecondaryDatabase needs to be initialized before log shipping setting can continue." -Target $SourceSqlInstance -Continue + } + + # Check the local backup path + if ($BackupLocalPath) { + if ($BackupLocalPath.EndsWith("\")) { + $DatabaseBackupLocalPath = "$BackupLocalPath$($db.Name)" + } + else { + $DatabaseBackupLocalPath = "$BackupLocalPath\$($db.Name)" + } + } + else { + $BackupLocalPath = $BackupNetworkPath + + if ($BackupLocalPath.EndsWith("\")) { + $DatabaseBackupLocalPath = "$BackupLocalPath$($db.Name)" + } + else { + $DatabaseBackupLocalPath = "$BackupLocalPath\$($db.Name)" + } + } + Write-Message -Message "Backup local path set to $DatabaseBackupLocalPath." -Level Verbose + + # Setting the backup network path for the database + if ($BackupNetworkPath.EndsWith("\")) { + $DatabaseBackupNetworkPath = "$BackupNetworkPath$($db.Name)" + } + else { + $DatabaseBackupNetworkPath = "$BackupNetworkPath\$($db.Name)" + } + Write-Message -Message "Backup network path set to $DatabaseBackupNetworkPath." -Level Verbose + + + # Checking if the database network path exists + Write-Message -Message "Testing database backup network path $DatabaseBackupNetworkPath" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseBackupNetworkPath -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + # To to create the backup directory for the database + try { + Write-Message -Message "Database backup network path $DatabaseBackupNetworkPath not found. Trying to create it.." -Level Verbose + + Invoke-Command2 -Credential $SourceCredential -ScriptBlock { + Write-Message -Message "Creating backup folder $DatabaseBackupNetworkPath" -Level Verbose + New-Item -Path $DatabaseBackupNetworkPath -ItemType Directory -Credential $SourceCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + + # Check if the backup job name is set + if ($BackupJob) { + $DatabaseBackupJob = "$BackupJob_$($db.Name)" + } + else { + $DatabaseBackupJob = "LSBackup_$($db.Name)" + } + Write-Message -Message "Backup job name set to $DatabaseBackupJob" -Level Verbose + + # Check if the backup job schedule name is set + if ($BackupSchedule) { + $DatabaseBackupSchedule = "$BackupSchedule_$($db.Name)" + } + else { + $DatabaseBackupSchedule = "LSBackupSchedule_$($db.Name)" + } + Write-Message -Message "Backup job schedule name set to $DatabaseBackupSchedule" -Level Verbose + + # Check if secondary database is present on secondary instance + if (-not $Force -and -not $NoInitialization -and ($DestinationServer.Databases[$SecondaryDatabase].Status -ne 'Restoring') -and ($DestinationServer.Databases.Name -contains $SecondaryDatabase)) { + Stop-Function -Message "Secondary database already exists on instance $DestinationSqlInstance." -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + + # Check if the secondary database needs tobe initialized + if (-not $NoInitialization) { + # Check if the secondary database exists on the secondary instance + if ($DestiationServer.Databases.Name -notcontains $SecondaryDatabase) { + # Check if force is being used and no option to generate the full backup is set + if ($Force -and -not ($GenerateFullBackup -or $UseExistingFullBackup)) { + # Set the option to generate a full backup + Write-Message -Message "Set option to initialize secondary database with full backup" -Level Verbose + $GenerateFullBackup = $true + } + elseif (-not $Force -and -not $GenerateFullBackup -and -not $UseExistingFullBackup -and -not $UseBackupFolder) { + # Set up the confirm part + $message = "The database $SecondaryDatabase does not exist on instance $DestinationSqlInstance. `nDo you want to initialize it by generating a full backup?" + $choiceYes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Answer Yes." + $choiceNo = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Answer No." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceYes, $choiceNo) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + # Check the result from the confirm + switch ($result) { + # If yes + 0 { + # Set the option to generate a full backup + Write-Message -Message "Set option to initialize secondary database with full backup." -Level Verbose + $GenerateFullBackup = $true + } + 1 { + Stop-Function -Message "The database is not initialized on the secondary instance. `nPlease initialize the database on the secondary instance, use -GenerateFullbackup or use -Force." -Target $DestinationSqlInstance + return + } + } # switch + } + } + } + + + # Check the parameters for initialization of the secondary database + if (-not $NoInitialization -and ($GenerateFullBackup -or $UseExistingFullBackup -or $UseBackupFolder)) { + # Check if the restore data and log folder are set + if (-not $RestoreDataFolder -or -not $RestoreLogFolder) { + Write-Message -Message "Restore data folder or restore log folder are not set. Using server defaults" -Level Verbose + + # Get the default data folder + if (-not $RestoreDataFolder) { + $DatabaseRestoreDataFolder = $DestinationServer.DefaultFile + } + else { + # Set the restore data folder + if ($RestoreDataFolder.EndsWith("\")) { + $DatabaseRestoreDataFolder = "$RestoreDataFolder$($db.Name)" + } + else { + $DatabaseRestoreDataFolder = "$RestoreDataFolder\$($db.Name)" + } + } + + Write-Message -Message "Restore data folder set to $DatabaseRestoreDataFolder" -Level Verbose + + # Get the default log folder + if (-not $RestoreLogFolder) { + $DatabaseRestoreLogFolder = $DestinationServer.DefaultLog + } + + Write-Message -Message "Restore log folder set to $DatabaseRestoreLogFolder" -Level Verbose + + # Check if the restore data folder exists + Write-Message -Message "Testing database restore data path $DatabaseRestoreDataFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseRestoreDataFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating database restore data folder $DatabaseRestoreDataFolder on $DestinationServerName")) { + # Try creating the data folder + try { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Creating data folder $DatabaseRestoreDataFolder" -Level Verbose + New-Item -Path $DatabaseRestoreDataFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the restore data directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + } + + # Check if the restore log folder exists + Write-Message -Message "Testing database restore log path $DatabaseRestoreLogFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseRestoreLogFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating database restore log folder $DatabaseRestoreLogFolder on $DestinationServerName")) { + # Try creating the log folder + try { + Write-Message -Message "Restore log folder $DatabaseRestoreLogFolder not found. Trying to create it.." -Level Verbose + + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Restore log folder $DatabaseRestoreLogFolder not found. Trying to create it.." -Level Verbose + New-Item -Path $DatabaseRestoreLogFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the restore log directory" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + } + } + + # Chech if the full backup patk can be reached + if ($FullBackupPath) { + Write-Message -Message "Testing full backup path $FullBackupPath" -Level Verbose + if ((Test-DbaSqlPath -Path $FullBackupPath -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message ("The path to the full backup could not be reached. Check the path and/or the crdential") -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + } + elseif ($UseBackupFolder.Length -ge 1) { + Write-Message -Message "Testing backup folder $UseBackupFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $UseBackupFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + Stop-Function -Message ("The path to the backup folder could not be reached. Check the path and/or the crdential") -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + + $BackupPath = $UseBackupFolder + } + elseif ($UseExistingFullBackup) { + Write-Message -Message "No path to the full backup is set. Trying to retrieve the last full backup for $db from $SourceSqlInstance" -Level Verbose + + # Get the last full backup + $LastBackup = Get-DbaBackupHistory -SqlServer $SourceSqlInstance -Databases $($db.Name) -LastFull -Credential $SourceSqlCredential + + # Check if there was a last backup + if ($LastBackup -ne $null) { + # Test the path to the backup + Write-Message -Message "Testing last backup path $(($LastBackup[-1]).Path[-1])" -Level Verbose + if ((Test-DbaSqlPath -Path ($LastBackup[-1]).Path[-1] -SqlInstance $SourceSqlInstance -SqlCredential $SourceCredential) -ne $true) { + Stop-Function -Message "The full backup could not be found on $($LastBackup.Path). Check path and/or credentials" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + # Check if the source for the last full backup is remote and the backup is on a shared location + elseif (($LastBackup.Computername -ne $SourceServerName) -and (($LastBackup[-1]).Path[-1].StartsWith('\\') -eq $false)) { + Stop-Function -Message "The last full backup is not located on shared location. `n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + else { + #$FullBackupPath = $LastBackup.Path + $BackupPath = $LastBackup.Path + Write-Message -Message "Full backup found for $db. Path $BackupPath" -Level Verbose + } + } + else { + Write-Message -Message "No Full backup found for $db." -Level Output + } + } + } + + # Set the copy destination folder to include the database name + if ($CopyDestinationFolder.EndsWith("\")) { + $DatabaseCopyDestinationFolder = "$CopyDestinationFolder$($db.Name)" + } + else { + $DatabaseCopyDestinationFolder = "$CopyDestinationFolder\$($db.Name)" + } + Write-Message -Message "Copy destination folder set to $DatabaseCopyDestinationFolder." -Level Verbose + + # Check if the copy job name is set + if ($CopyJob) { + $DatabaseCopyJob = "$CopyJob_$SourceServerName_$($db.Name)" + } + else { + $DatabaseCopyJob = "LSCopy_$SourceServerName_$($db.Name)" + } + Write-Message -Message "Copy job name set to $DatabaseCopyJob" -Level Verbose + + # Check if the copy job schedule name is set + if ($CopySchedule) { + $DatabaseCopySchedule = "$CopySchedule_$($db.Name)" + } + else { + $DatabaseCopySchedule = "LSCopySchedule_$($db.Name)" + Write-Message -Message "Copy job schedule name set to $DatabaseCopySchedule" -Level Verbose + } + + # Check if the copy destination folder exists + Write-Message -Message "Testing database copy destination path $DatabaseCopyDestinationFolder" -Level Verbose + if ((Test-DbaSqlPath -Path $DatabaseCopyDestinationFolder -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationCredential) -ne $true) { + if ($PSCmdlet.ShouldProcess($DestinationServerName, "Creating copy destination folder on $DestinationServerName")) { + try { + Invoke-Command2 -Credential $DestinationCredential -ScriptBlock { + Write-Message -Message "Copy destination folder $DatabaseCopyDestinationFolder not found. Trying to create it.. ." -Level Verbose + New-Item -Path $DatabaseCopyDestinationFolder -ItemType Directory -Credential $DestinationCredential -Force:$Force | Out-Null + } + } + catch { + Stop-Function -Message "Something went wrong creating the database copy destination folder. `n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationServerName -Continue + } + } + } + + # Check if the restore job name is set + if ($RestoreJob) { + $DatabaseRestoreJob = "$RestoreJob_$SourceServerName_$($db.Name)" + } + else { + $DatabaseRestoreJob = "LSRestore_$DestinationServerName_$($db.Name)" + } + Write-Message -Message "Restore job name set to $DatabaseRestoreJob" -Level Verbose + + # Check if the restore job schedule name is set + if ($RestoreSchedule) { + $DatabaseRestoreSchedule = "$RestoreSchedule_$($db.Name)" + } + else { + $DatabaseRestoreSchedule = "LSRestoreSchedule_$($db.Name)" + } + Write-Message -Message "Restore job schedule name set to $DatabaseRestoreSchedule" -Level Verbose + + # If the database needs to be backed up first + if ($GenerateFullBackup) { + if ($PSCmdlet.ShouldProcess($SourceSqlInstance, "Backing up database $db")) { + + Write-Message -Message "Generating full backup." -Level Output + Write-Message -Message "Backing up database $db to $DatabaseBackupNetworkPath" -Level Output + + try { + $Timestamp = Get-Date -format "yyyyMMddHHmmss" + + $LastBackup = Backup-DbaDatabase -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -BackupDirectory $DatabaseBackupNetworkPath ` + -BackupFileName "FullBackup_$($db.Name)_PreLogShipping_$Timestamp.bak" ` + -Databases $($db.Name) ` + -Type Full + + Write-Message -Message "Backup completed." -Level Output + + # Get the last full backup path + #$FullBackupPath = $LastBackup.BackupPath + $BackupPath = $LastBackup.BackupPath + + Write-Message -Message "Backup is located at $BackupPath" -Level Verbose + } + catch { + Stop-Function -Message "Something went wrong generating the full backup" -ErrorRecord $_ -Target $DestinationServerName -Continue + } + } + } + + # Check of the MonitorServerSecurityMode value is of type string and set the integer value + if ($PrimaryMonitorServerSecurityMode -notin 0, 1) { + $PrimaryMonitorServerSecurityMode = switch ($PrimaryMonitorServerSecurityMode) { + "SQLSERVER" { 0 } "WINDOWS" { 1 } default { 1 } + } + } + + # Check the primary monitor server + if ($Force -and (-not$PrimaryMonitorServer -or [string]$PrimaryMonitorServer -eq '' -or $PrimaryMonitorServer -eq $null)) { + Write-Message -Message "Setting monitor server for primary server to $SourceSqlInstance." -Level Output + $PrimaryMonitorServer = $SourceSqlInstance + } + + # Check the PrimaryMonitorServerSecurityMode if it's SQL Server authentication + if ($PrimaryMonitorServerSecurityMode -eq 0) { + if ($PrimaryMonitorServerLogin) { + Stop-Function -Message "The PrimaryMonitorServerLogin cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + + if ($PrimaryMonitorServerPassword) { + Stop-Function -Message "The PrimaryMonitorServerPassword cannot be empty when using SQL Server authentication." -Target $ -Continue + } + } + + # Check of the SecondaryMonitorServerSecurityMode value is of type string and set the integer value + if ($SecondaryMonitorServerSecurityMode -notin 0, 1) { + $SecondaryMonitorServerSecurityMode = switch ($SecondaryMonitorServerSecurityMode) { + "SQLSERVER" { 0 } "WINDOWS" { 1 } default { 1 } + } + } + + # Check the secondary monitor server + if ($Force -and (-not $SecondaryMonitorServer -or [string]$SecondaryMonitorServer -eq '' -or $SecondaryMonitorServer -eq $null)) { + Write-Message -Message "Setting secondary monitor server for $DestinationSqlInstance to $SourceSqlInstance." -Level Verbose + $SecondaryMonitorServer = $SourceSqlInstance + } + + # Check the MonitorServerSecurityMode if it's SQL Server authentication + if ($SecondaryMonitorServerSecurityMode -eq 0) { + if ($SecondaryMonitorServerLogin) { + Stop-Function -Message "The SecondaryMonitorServerLogin cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + + if ($SecondaryMonitorServerPassword) { + Stop-Function -Message "The SecondaryMonitorServerPassword cannot be empty when using SQL Server authentication." -Target $SourceSqlInstance -Continue + } + } + + # Now that all the checks have been done we can start with the fun stuff ! + + # Restore the full backup + if ($PSCmdlet.ShouldProcess($DestinationSqlInstance, "Restoring database $db to $SecondaryDatabase on $DestinationSqlInstance")) { + if ($GenerateFullBackup -or $UseExistingFullBackup -or $UseBackupFolder) { + try { + Write-Message -Message "Start database restore" -Level Output + if ($NoRecovery -or (-not $Standby)) { + if ($Force) { + Restore-DbaDatabase -SqlServer $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -NoRecovery ` + -WithReplace | Out-Null + } + else { + Restore-DbaDatabase -SqlServer $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -NoRecovery | Out-Null + } + } + + # If the database needs to be in standby + if ($Standby) { + # Setup the path to the standby file + $StandbyDirectory = "$DatabaseCopyDestinationFolder" + + # Check if credentials need to be used + if ($DestinationSqlCredential) { + Restore-DbaDatabase -ServerInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -StandbyDirectory $StandbyDirectory + } + else { + Restore-DbaDatabase -ServerInstance $DestinationSqlInstance ` + -Path $BackupPath ` + -DestinationFilePrefix $SecondaryDatabasePrefix ` + -DestinationFileSuffix $SecondaryDatabaseSuffix ` + -DestinationDataDirectory $DatabaseRestoreDataFolder ` + -DestinationLogDirectory $DatabaseRestoreLogFolder ` + -DatabaseName $SecondaryDatabase ` + -DirectoryRecurse ` + -StandbyDirectory $StandbyDirectory + } + } + } + catch { + Stop-Function -Message "Something went wrong restoring the secondary database" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + + Write-Message -Message "Restore completed." -Level Output + } + } + + #region Set up log shipping on the primary instance + # Set up log shipping on the primary instance + if ($PSCmdlet.ShouldProcess($SourceSqlInstance, "Configuring logshipping for primary database $db on $SourceSqlInstance")) { + try { + + Write-Message -Message "Configuring logshipping for primary database" -Level Output + + New-DbaLogShippingPrimaryDatabase -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -Database $($db.Name) ` + -BackupDirectory $DatabaseBackupLocalPath ` + -BackupJob $DatabaseBackupJob ` + -BackupRetention $BackupRetention ` + -BackupShare $DatabaseBackupNetworkPath ` + -BackupThreshold $BackupThreshold ` + -CompressBackup:$BackupCompression ` + -HistoryRetention $HistoryRetention ` + -MonitorServer $PrimaryMonitorServer ` + -MonitorServerSecurityMode $PrimaryMonitorServerSecurityMode ` + -MonitorCredential $PrimaryMonitorCredential ` + -ThresholdAlertEnabled:$PrimaryThresholdAlertEnabled ` + -Force:$Force + + # Check if the backup job needs to be enabled or disabled + if ($BackupScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Job $DatabaseBackupJob -Disabled + Write-Message -Message "Disabling backup job $DatabaseBackupJob" -Level Output + } + else { + Set-DbaAgentJob -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Job $DatabaseBackupJob -Enabled + Write-Message -Message "Enabling backup job $DatabaseBackupJob" -Level Output + } + + Write-Message -Message "Create backup job schedule $DatabaseBackupSchedule" -Level Output + + $BackupJobSchedule = New-DbaAgentSchedule -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -Job $DatabaseBackupJob ` + -Schedule $DatabaseBackupSchedule ` + -FrequencyType $BackupScheduleFrequencyType ` + -FrequencyInterval $BackupScheduleFrequencyInterval ` + -FrequencySubdayType $BackupScheduleFrequencySubdayType ` + -FrequencySubdayInterval $BackupScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $BackupScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $BackupScheduleFrequencyRecurrenceFactor ` + -StartDate $BackupScheduleStartDate ` + -EndDate $BackupScheduleEndDate ` + -StartTime $BackupScheduleStartTime ` + -EndTime $BackupScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Configuring logshipping from primary to secondary database." -Level Output + + New-DbaLogShippingPrimarySecondary -SqlInstance $SourceSqlInstance ` + -SqlCredential $SourceSqlCredential ` + -PrimaryDatabase $($db.Name) ` + -SecondaryDatabase $SecondaryDatabase ` + -SecondaryServer $DestinationSqlInstance ` + -SecondarySqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Something went wrong setting up log shipping for primary instance" -ErrorRecord $_ -Target $SourceSqlInstance -Continue + } + } + #endregion Set up log shipping on the primary instance + + #region Set up log shipping on the secondary instance + # Set up log shipping on the secondary instance + if ($PSCmdlet.ShouldProcess($DestinationSqlInstance, "Configuring logshipping for secondary database $SecondaryDatabase on $DestinationSqlInstance")) { + try { + + Write-Message -Message "Configuring logshipping from secondary database $SecondaryDatabase to primary database $db." -Level Output + + New-DbaLogShippingSecondaryPrimary -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -BackupSourceDirectory $DatabaseBackupNetworkPath ` + -BackupDestinationDirectory $DatabaseCopyDestinationFolder ` + -CopyJob $DatabaseCopyJob ` + -FileRetentionPeriod $BackupRetention ` + -MonitorServer $SecondaryMonitorServer ` + -MonitorServerSecurityMode $SecondaryMonitorServerSecurityMode ` + -MonitorCredential $SecondaryMonitorCredential ` + -PrimaryServer $SourceSqlInstance ` + -PrimaryDatabase $($db.Name) ` + -RestoreJob $DatabaseRestoreJob ` + -Force:$Force + + Write-Message -Message "Create copy job schedule $DatabaseCopySchedule" -Level Output + + $CopyJobSchedule = New-DbaAgentSchedule -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Job $DatabaseCopyJob ` + -Schedule $DatabaseCopySchedule ` + -FrequencyType $CopyScheduleFrequencyType ` + -FrequencyInterval $CopyScheduleFrequencyInterval ` + -FrequencySubdayType $CopyScheduleFrequencySubdayType ` + -FrequencySubdayInterval $CopyScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $CopyScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $CopyScheduleFrequencyRecurrenceFactor ` + -StartDate $CopyScheduleStartDate ` + -EndDate $CopyScheduleEndDate ` + -StartTime $CopyScheduleStartTime ` + -EndTime $CopyScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Create restore job schedule $DatabaseRestoreSchedule" -Level Output + + $RestoreJobSchedule = New-DbaAgentSchedule -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -Job $DatabaseRestoreJob ` + -Schedule $DatabaseRestoreSchedule ` + -FrequencyType $RestoreScheduleFrequencyType ` + -FrequencyInterval $RestoreScheduleFrequencyInterval ` + -FrequencySubdayType $RestoreScheduleFrequencySubdayType ` + -FrequencySubdayInterval $RestoreScheduleFrequencySubdayInterval ` + -FrequencyRelativeInterval $RestoreScheduleFrequencyRelativeInterval ` + -FrequencyRecurrenceFactor $RestoreScheduleFrequencyRecurrenceFactor ` + -StartDate $RestoreScheduleStartDate ` + -EndDate $RestoreScheduleEndDate ` + -StartTime $RestoreScheduleStartTime ` + -EndTime $RestoreScheduleEndTime ` + -Force:$Force + + Write-Message -Message "Configuring logshipping for secondary database." -Level Output + + New-DbaLogShippingSecondaryDatabase -SqlInstance $DestinationSqlInstance ` + -SqlCredential $DestinationSqlCredential ` + -SecondaryDatabase $SecondaryDatabase ` + -PrimaryServer $SourceSqlInstance ` + -PrimaryDatabase $($db.Name) ` + -RestoreDelay $RestoreDelay ` + -RestoreMode $DatabaseStatus ` + -DisconnectUsers:$DisconnectUsers ` + -RestoreThreshold $RestoreThreshold ` + -ThresholdAlertEnabled:$SecondaryThresholdAlertEnabled ` + -HistoryRetention $HistoryRetention + + # Check if the copy job needs to be enabled or disabled + if ($CopyScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseCopyJob -Disabled + } + else { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseCopyJob -Enabled + } + + # Check if the restore job needs to be enabled or disabled + if ($RestoreScheduleDisabled) { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseRestoreJob -Disabled + } + else { + Set-DbaAgentJob -SqlInstance $DestinationSqlInstance -SqlCredential $DestinationSqlCredential -Job $DatabaseRestoreJob -Enabled + } + + } + catch { + Stop-Function -Message "Something went wrong setting up log shipping for secondary instance.`n$($_.Exception.Message)" -ErrorRecord $_ -Target $DestinationSqlInstance -Continue + } + } + #endregion Set up log shipping on the secondary instance + + Write-Message -Message "Completed configuring log shipping for database $db" -Level Output + + } # for each database + } # end process + + end { + Write-Message -Message "Finished setting up log shipping." -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaLogShippingRecovery.ps1 b/functions/Invoke-DbaLogShippingRecovery.ps1 new file mode 100644 index 000000000000..05c73e75c47b --- /dev/null +++ b/functions/Invoke-DbaLogShippingRecovery.ps1 @@ -0,0 +1,394 @@ +function Invoke-DbaLogShippingRecovery { + <# + .SYNOPSIS + Invoke-DbaLogShippingRecovery recovers log shipped databases to a normal state to act upon a migration or disaster. + + .DESCRIPTION + By default all the databases for a particular instance are recovered. + If the database is in the right state, either standby or recovering, the process will try to recover the database. + + At first the function will check if the backup source directory can still be reached. + If so it will look up the last transaction log backup for the database. If that backup file is not the last copied file the log shipping copy job will be started. + If the directory cannot be reached for the function will continue to the restoring process. + After the copy job check is performed the job is disabled to prevent the job to run. + + For the restore the log shipping status is checked in the msdb database. + If the last restored file is not the same as the last file name found, the log shipping restore job will be executed. + After the restore job check is performed the job is disabled to prevent the job to run + + The last part is to set the database online by restoring the databases with recovery + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to + + .PARAMETER Database + Database to perform the restore for. This value can also be piped enabling multiple databases to be recovered. + If this value is not supplied all databases will be recovered. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER NoRecovery + Allows you to choose to not restore the database to a functional state (Normal) in the final steps of the process. + By default the database is restored to a functional state (Normal). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Force + Use this parameter to force the function to continue and perform any adjusting actions to successfully execute + + .PARAMETER Delay + Set the delay in seconds to wait for the copy and/or restore jobs. + By default the delay is 5 seconds + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: LogShipping + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaLogShippingRecovery + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 + + Recovers all the databases on the instance that are enabled for log shipping + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -SqlCredential $cred -Verbose + + Recovers all the databases on the instance that are enabled for log shipping using a credential + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -database db_logship -Verbose + + Recovers the database "db_logship" to a normal status + + .EXAMPLE + db1, db2, db3, db4 | Invoke-DbaLogShippingRecovery -SqlServer server1 -Verbose + + Recovers the database db1, db2, db3, db4 to a normal status + + .EXAMPLE + Invoke-DbaLogShippingRecovery -SqlServer server1 -WhatIf + + Shows what would happen if the command were executed. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [object]$SqlInstance, + [Parameter(ValueFromPipeline = $true)] + [object[]]$Database, + [PSCredential]$SqlCredential, + [switch]$NoRecovery, + [Alias('Silent')] + [switch]$EnableException, + [switch]$Force, + [int]$Delay = 5 + ) + + begin { + if (!$sqlinstance -and $database.Count -lt 1) { + # You can prolly do this with + Stop-Function -Message "You must pipe an SMO database object or specify SqlInstance" + return + } + + if ($sqlinstance) { + # Check the instance if it is a named instance + $servername, $instancename = $sqlinstance.Split("\") + + if ($null -eq $instancename) { + $instancename = "MSSQLSERVER" + } + + Write-Message -Message "Attempting to connect to Sql Server" -Level Output + try { + $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance + } + + if ($Force -and (!$database -or $database.Count -lt 1)) { + $database = $server.databases + } + elseif (-not $Force -and (!$database -or $database.Count -lt 1)) { + Stop-Function -Message "Please enter one or more databases to recover from log shipping" -Target $instance + } + else { + $databases = $server.databases | Where-Object Name -in $database + } + } + } + + process { + # Try to get the agent service details + try { + # Start the service + $agentservice = Get-DbaSqlService -ComputerName $servername | Where-Object {($_.ComputerName -eq $servername) -and ($_.DisplayName -eq "SQL Server Agent ($instancename)")} + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + + # Check if the service is running + if ($agentservice.State -ne 'Running') { + + if ($Force) { + try { + Start-DbaSqlService -ComputerName $servername -InstanceName $instancename -Type Agent -Credential $SqlCredential + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + } + # If the force switch and the silent switch are not set + elseif (!$Force -and !$EnableException) { + # Set up the parts for the user choice + $Title = "SQL Server Agent is not running" + $Info = "Do you want to start the SQL Server Agent service?" + + $Options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Start", "&Quit") + [int]$Defaultchoice = 0 + $choice = $host.UI.PromptForChoice($Title, $Info, $Options, $Defaultchoice) + + # Check the given option + if ($choice -eq 0) { + try { + # Start the service + Start-DbaSqlService -ComputerName $servername -InstanceName $instancename -Type Agent -Credential $SqlCredential + } + catch { + # Stop the function when the service was unable to start + Stop-Function -Message "Unable to start SQL Server Agent Service" -ErrorRecord $_ -Target $sqlinstance + return + } + } + else { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + } + # If the force switch it not set and the silent switch is set + elseif (!$Force -and $EnableException) { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + # If nothing else matches and the agent service is not started + else { + Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -ErrorRecord $_ -Target $sqlinstance + return + } + + } + + Write-Message -Message "Started Log Shipping Recovery" -Level Output + + # Loop through all the databases + foreach ($db in $databases) { + # Query for retrieving the log shipping information + $query = "SELECT lss.primary_server, lss.primary_database, lsd.secondary_database, lss.backup_source_directory, + lss.backup_destination_directory, lss.last_copied_file, lss.last_copied_date, + lsd.last_restored_file, sj1.name AS 'copyjob', sj2.name AS 'restorejob' + FROM msdb.dbo.log_shipping_secondary AS lss + INNER JOIN msdb.dbo.log_shipping_secondary_databases AS lsd ON lsd.secondary_id = lss.secondary_id + INNER JOIN msdb.dbo.sysjobs AS sj1 ON sj1.job_id = lss.copy_job_id + INNER JOIN msdb.dbo.sysjobs AS sj2 ON sj2.job_id = lss.restore_job_id + WHERE lsd.secondary_database = '$($db.Name)'" + + # Retrieve the log shipping information from the secondary instance + try { + Write-Message -Message "Retrieving log shipping information from the secondary instance" -Level Verbose + $logshipping_details = $server.Query($query) + } + catch { + Stop-Function -Message "Error retrieving the log shipping details: $($_.Exception.Message)" -ErrorRecord $_ -Target $sqlinstance + return + } + + # Check if there are any databases to recover + if ($null -eq $logshipping_details) { + Stop-Function -Message "The database $db is not configured as a secondary database for log shipping." -Continue + } + else { + # Loop through each of the log shipped databases + foreach ($ls in $logshipping_details) { + $secondarydb = $ls.secondary_database + + # Check if the database is in the right state + if ($server.Databases[$secondarydb].Status -notin ('Normal, Standby', 'Standby', 'Restoring')) { + Stop-Function -Message "The database $db doesn't have the right status to be recovered" -Continue + } + else { + Write-Message -Message "Started Recovery for $secondarydb" -Level Verbose + + # Get the last file from the backup source directory + <# !!!! set credentials !!! #> + $latestBackupSource = Get-ChildItem -Path $ls.backup_source_directory -filter ("*" + $ls.primary_database + "*") | Where-Object { ($_.Extension -eq '.trn') } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + # Get al the backup files from the destination directory + <# !!!! set credentials !!! #> + $latestBackupDest = Get-ChildItem -Path $ls.backup_destination_directory -filter ("*" + $ls.primary_database + "*") | Where-Object { ($_.Extension -eq '.trn') } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + # Check if source and destination directory are in sync + if ($latestBackupSource.Name -ne $latestBackupDest.Name) { + # Check if the backup source directory can be reached + if (Test-DbaSqlPath -SqlInstance $SqlInstance -Path $ls.backup_source_directory -SqlCredential $SqlCredential) { + + # Check if the latest file is also the latest copied file + if ($latestBackupSource.Name -ne ([string]$ls.last_copied_file).Split('\')[-1]) { + Write-Message -Message "Backup destination is not up-to-date" -Level Verbose + + # Start the job to get the latest files + if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting copy job $($ls.copyjob)"))) { + Write-Message -Message "Starting copy job $($ls.copyjob)" -Level Verbose + try { + $server.JobServer.Jobs[$ls.copyjob].Start() + } + catch { + Stop-Function -Message "Something went wrong starting the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + Write-Message -Message "Copying files to $($ls.backup_destination_directory)" -Level Verbose + + # Check if the file has been copied + $query = "SELECT last_copied_file FROM msdb.dbo.log_shipping_secondary WHERE primary_database = '$($ls.primary_database)' AND last_copied_file IS NOT NULL " + $latestcopy = $server.Query($query) + + Write-Message -Message "Waiting for the copy action to complete.." -Level Verbose + + while (($latestBackupSource.Name -ne ([string]$latestcopy.last_copied_file).Split('\')[-1])) { + # Sleep for while to let the files be copied + Start-Sleep -Seconds $Delay + + # Again get the latest file to check if the process can continue + $latestcopy = $server.Query($query) + } + + # Again get the latest file to check if the process can continue + $latestcopy = $server.Query($query) + + # Check the lat outcome of the job + if ($server.JobServer.Jobs[$ls.copyjob].LastRunOutcome -eq 'Failed') { + Stop-Function -Message "The copy job for database $db failed. Please check the error log." -Continue + } + + Write-Message -Message "Copying of backup files finished" -Level Verbose + } # if should process + } # if latest file name + } # if backup directory test + else { + Stop-Function -Message "Couldn't reach the backup source directory. Continuing..." -Continue + } + } # check latest backup file is already in directory + + + # Disable the log shipping copy job on the secondary instance + if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling copy job $($ls.copyjob)")) { + try { + Write-Message -Message "Disabling copy job $($ls.copyjob)" -Level Verbose + $server.JobServer.Jobs[$ls.copyjob].IsEnabled = $false + $server.JobServer.Jobs[$ls.copyjob].Alter() + } + catch { + Stop-Function -Message "Something went wrong disabling the copy job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + } + + # Check if the file has been copied + $query = "SELECT last_restored_file FROM msdb.dbo.log_shipping_secondary_databases WHERE secondary_database = '$secondarydb' AND last_restored_file IS NOT NULL" + $latestrestore = $server.Query($query) + + # Check if the last copied file is newer than the last restored file + if ((([string]$latestcopy.last_copied_file).Split('\')[-1] -ne ([string]$latestrestore.last_restored_file).Split('\')[-1]) -or ($null -eq ([string]$latestcopy.last_copied_file).Split('\')[-1])) { + Write-Message -Message "Restore is not up-to-date" -Level Verbose + + # Start the restore job + if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting restore job " + $ls.restorejob))) { + Write-Message -Message "Starting restore job $($ls.restorejob)" -Level Verbose + try { + $server.JobServer.Jobs[$ls.restorejob].Start() + } + catch { + Stop-Function -Message "Something went wrong starting the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + Write-Message -Message "Waiting for the restore action to complete.." -Level Verbose + + while ($latestBackupSource.Name -ne [string]($latestrestore.last_restored_file).Split('\')[-1]) { + # Sleep for while to let the files be copied + Start-Sleep -Seconds $Delay + + # Again get the latest file to check if the process can continue + $latestrestore = $server.Query($query) + } + + # Again get the latest file to check if the process can continue + $latestrestore = $server.Query($query) + + # Check the lat outcome of the job + if ($server.JobServer.Jobs[$ls.restorejob].LastRunOutcome -eq 'Failed') { + Stop-Function -Message "The restore job for database $db failed. Please check the error log." -Continue + } + } + } + + # Disable the log shipping restore job on the secondary instance + if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling restore job $($ls.restorejob)")) { + try { + Write-Message -Message ("Disabling restore job " + $ls.restorejob) -Level Verbose + $server.JobServer.Jobs[$ls.restorejob].IsEnabled = $false + $server.JobServer.Jobs[$ls.restorejob].Alter() + } + catch { + Stop-Function -Message "Something went wrong disabling the restore job.`n$($_)" -ErrorRecord $_ -Target $sqlinstance + } + + } + + # Check for the last time if everything is up-to-date + if ($latestBackupSource.Name -eq [string]($latestrestore.last_restored_file).Split('\')[-1]) { + # Check if the database needs to recovered to its normal state + if ($NoRecovery -eq $false) { + if ($PSCmdlet.ShouldProcess($secondarydb, "Restoring database with recovery")) { + Write-Message -Message "Restoring the database to it's normal state" -Level Verbose + $query = "RESTORE DATABASE [$secondarydb] WITH RECOVERY" + $server.Query($query) + } + } + else { + Write-Message -Message "Skipping restore with recovery" -Level Output + } + } + + Write-Message -Message ("Finished Recovery for $secondarydb") -Level Output + + # Reset the log ship details + $logshipping_details = $null + + } # database in restorable mode + } # foreach ls details + } # ls details are not null + } # foreach database + } # process +} \ No newline at end of file diff --git a/functions/Invoke-DbaPfRelog.ps1 b/functions/Invoke-DbaPfRelog.ps1 new file mode 100644 index 000000000000..3da5769b6167 --- /dev/null +++ b/functions/Invoke-DbaPfRelog.ps1 @@ -0,0 +1,439 @@ +function Invoke-DbaPfRelog { + <# + .SYNOPSIS + Pipeline-compatible wrapper for the relog command which is available on modern Windows platforms. + + .DESCRIPTION + Pipeline-compatible wrapper for the relog command. Relog is useful for converting Windows Perfmon. + + Extracts performance counters from performance counter logs into other formats, + such as text-TSV (for tab-delimited text), text-CSV (for comma-delimited text), binary-BIN, or SQL. + + relog "C:\PerfLogs\Admin\System Correlation\WORKSTATIONX_20180112-000001\DataCollector01.blg" -o C:\temp\foo.csv -f tsv + + If you find any input hangs, please send us the output so we can accommodate for it then use -Raw for an immediate solution. + + .PARAMETER Path + Specifies the pathname of an existing performance counter log or performance counter path. You can specify multiple input files. + + .PARAMETER Destination + Specifies the pathname of the output file or SQL database where the counters will be written. Defaults to the same directory as the source. + + .PARAMETER Type + The output format. Defaults to tsv. Options include tsv, csv, bin, and sql. + + For a SQL database, the output file specifies the DSN!counter_log. You can specify the database location by using the ODBC manager to configure the DSN (Database System Name). + + For more information, read here: https://technet.microsoft.com/en-us/library/bb490958.aspx + + .PARAMETER Append + If this switch is enabled, output will be appended to the specified file instead of overwriting. This option does not apply to SQL format where the default is always to append. + + .PARAMETER AllowClobber + If this switch is enabled, the destination file will be overwritten if it exists. + + .PARAMETER PerformanceCounter + Specifies the performance counter path to log. + + .PARAMETER PerformanceCounterPath + Specifies the pathname of the text file that lists the performance counters to be included in a relog file. Use this option to list counter paths in an input file, one per line. Default setting is all counters in the original log file are relogged. + + .PARAMETER Interval + Specifies sample intervals in "n" records. Includes every nth data point in the relog file. Default is every data point. + + .PARAMETER BeginTime + This is is Get-Date object and we format it for you. + + .PARAMETER EndTime + Specifies end time for copying last record from the input file. This is is Get-Date object and we format it for you. + + .PARAMETER ConfigPath + Specifies the pathname of the settings file that contains command-line parameters. + + .PARAMETER Summary + If this switch is enabled, the performance counters and time ranges of log files specified in the input file will be displayed. + + .PARAMETER Multithread + If this switch is enabled, processing will be done in parallel. This may speed up large batches or large files. + + .PARAMETER AllTime + If this switch is enabled and a datacollector or datacollectorset is passed in via the pipeline, collects all logs, not just the latest. + + .PARAMETER Raw + If this switch is enabled, the results of the DOS command instead of Get-ChildItem will be displayed. This does not run in parallel. + + .PARAMETER InputObject + Accepts the output of Get-DbaPfDataCollector and Get-DbaPfDataCollectorSet as input via the pipeline. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Performance, DataCollector, PerfCounter, Relog + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaPfRelog + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg + + Creates C:\temp\perfmon.tsv from C:\temp\perfmon.blg. + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg -Destination C:\temp\a\b\c + + Creates the temp, a, and b directories if needed, then generates c.tsv (tab separated) from C:\temp\perfmon.blg. + + Returns the newly created file as a file object. + + .EXAMPLE + Get-DbaPfDataCollectorSet -ComputerName sql2016 | Get-DbaPfDataCollector | Invoke-DbaPfRelog -Destination C:\temp\perf + + Creates C:\temp\perf if needed, then generates computername-datacollectorname.tsv (tab separated) from the latest logs of all data collector sets on sql2016. This destination format was chosen to avoid naming conflicts with piped input. + + .EXAMPLE + Invoke-DbaPfRelog -Path C:\temp\perfmon.blg -Destination C:\temp\a\b\c -Raw + + Creates the temp, a, and b directories if needed, then generates c.tsv (tab separated) from C:\temp\perfmon.blg then outputs the raw results of the relog command. + + [Invoke-DbaPfRelog][21:21:35] relog "C:\temp\perfmon.blg" -f csv -o C:\temp\a\b\c + + Input + ---------------- + File(s): + C:\temp\perfmon.blg (Binary) + + Begin: 1/13/2018 5:13:23 + End: 1/13/2018 14:29:55 + Samples: 2227 + + 100.00% + + Output + ---------------- + File: C:\temp\a\b\c.csv + + Begin: 1/13/2018 5:13:23 + End: 1/13/2018 14:29:55 + Samples: 2227 + + The command completed successfully. + + .EXAMPLE + Invoke-DbaPfRelog -Path 'C:\temp\perflog with spaces.blg' -Destination C:\temp\a\b\c -Type csv -BeginTime ((Get-Date).AddDays(-30)) -EndTime ((Get-Date).AddDays(-1)) + + Creates the temp, a, and b directories if needed, then generates c.csv (comma separated) from C:\temp\perflog with spaces.blg', starts 30 days ago and ends one day ago. + + .EXAMPLE + $servers | Get-DbaPfDataCollectorSet | Get-DbaPfDataCollector | Invoke-DbaPfRelog -Multithread -AllowClobber + + Relogs latest data files from all collectors within the servers listed in $servers. + + .EXAMPLE + Get-DbaPfDataCollector -Collector DataCollector01 | Invoke-DbaPfRelog -AllowClobber -AllTime + + Relogs all the log files from the DataCollector01 on the local computer and allows overwrite. + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipelineByPropertyName)] + [Alias("FullName")] + [string[]]$Path, + [string]$Destination, + [ValidateSet("tsv", "csv", "bin", "sql")] + [string]$Type = "tsv", + [switch]$Append, + [switch]$AllowClobber, + [string[]]$PerformanceCounter, + [string]$PerformanceCounterPath, + [int]$Interval, + [datetime]$BeginTime, + [datetime]$EndTime, + [string]$ConfigPath, + [switch]$Summary, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$Multithread, + [switch]$AllTime, + [switch]$Raw, + [switch]$EnableException + ) + begin { + if (Test-Bound -ParameterName BeginTime) { + $script:beginstring = ($BeginTime -f 'M/d/yyyy hh:mm:ss' | Out-String).Trim() + } + if (Test-Bound -ParameterName EndTime) { + $script:endstring = ($EndTime -f 'M/d/yyyy hh:mm:ss' | Out-String).Trim() + } + + $allpaths = @() + $allpaths += $Path + + # to support multithreading + if (Test-Bound -ParameterName Destination) { + $script:destinationset = $true + $originaldestination = $Destination + } + else { + $script:destinationset = $false + } + } + process { + if ($Append -and $Type -ne "bin") { + Stop-Function -Message "Append can only be used with -Type bin." -Target $Path + return + } + + if ($InputObject) { + foreach ($object in $InputObject) { + # DataCollectorSet + if ($object.OutputLocation -and $object.RemoteOutputLocation) { + $instance = [dbainstance]$object.ComputerName + + if (-not $AllTime) { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.LatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteLatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + else { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.OutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + + + $script:perfmonobject = $true + } + # DataCollector + if ($object.LatestOutputLocation -and $object.RemoteLatestOutputLocation) { + $instance = [dbainstance]$object.ComputerName + + if (-not $AllTime) { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path $object.LatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path $object.RemoteLatestOutputLocation -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + else { + if ($instance.IsLocalHost) { + $allpaths += (Get-ChildItem -Recurse -Path (Split-Path $object.LatestOutputLocation) -Include *.blg -ErrorAction SilentlyContinue).FullName + } + else { + $allpaths += (Get-ChildItem -Recurse -Path (Split-Path $object.RemoteLatestOutputLocation) -Include *.blg -ErrorAction SilentlyContinue).FullName + } + } + $script:perfmonobject = $true + } + } + } + } + + # Gotta collect all the paths first then process them otherwise there may be duplicates + end { + $allpaths = $allpaths | Where-Object { $_ -match '.blg' } | Select-Object -Unique + + if (-not $allpaths) { + Stop-Function -Message "Could not find matching .blg files" -Target $file -Continue + return + } + + $scriptblock = { + if ($args) { + $file = $args + } + else { + $file = $psitem + } + $item = Get-ChildItem -Path $file -ErrorAction SilentlyContinue + + if ($null -eq $item) { + Stop-Function -Message "$file does not exist." -Target $file -Continue + return + } + + if (-not $script:destinationset -and $file -match "C\:\\.*Admin.*") { + $null = Test-ElevationRequirement -ComputerName $env:COMPUTERNAME -Continue + } + + if ($script:destinationset -eq $false -and -not $Append) { + $Destination = Join-Path (Split-Path $file) $item.BaseName + } + + if ($Destination -and $Destination -notmatch "\." -and -not $Append -and $script:perfmonobject) { + # if destination is set, then it needs a different name + if ($script:destinationset -eq $true) { + if ($file -match "\:") { + $computer = $env:COMPUTERNAME + } + else { + $computer = $file.Split("\")[2] + } + # Avoid naming conflicts + $timestamp = Get-Date -format yyyyMMddHHmmfff + $Destination = Join-Path $originaldestination "$computer - $($item.BaseName) - $timestamp" + } + } + + $params = @("`"$file`"") + + if ($Append) { + $params += "-a" + } + + if ($PerformanceCounter) { + $parsedcounters = $PerformanceCounter -join " " + $params += "-c `"$parsedcounters`"" + } + + if ($PerformanceCounterPath) { + $params += "-cf `"$PerformanceCounterPath`"" + } + + $params += "-f $Type" + + if ($Interval) { + $params += "-t $Interval" + } + + if ($Destination) { + $params += "-o `"$Destination`"" + } + + if ($script:beginstring) { + $params += "-b $script:beginstring" + } + + if ($script:endstring) { + $params += "-e $script:endstring" + } + + if ($ConfigPath) { + $params += "-config $ConfigPath" + } + + if ($Summary) { + $params += "-q" + } + + + if (-not ($Destination.StartsWith("DSN"))) { + $outputisfile = $true + } + else { + $outputisfile = $false + } + + if ($outputisfile) { + if ($Destination) { + $dir = Split-Path $Destination + if (-not (Test-Path -Path $dir)) { + try { + $null = New-Item -ItemType Directory -Path $dir -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $Destination -Continue + } + } + + if ((Test-Path $Destination) -and -not $Append -and ((Get-Item $Destination) -isnot [System.IO.DirectoryInfo])) { + if ($AllowClobber) { + try { + Remove-Item -Path "$Destination" -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Continue + } + } + else { + if ($Type -eq "bin") { + Stop-Function -Message "$Destination exists. Use -AllowClobber to overwrite or -Append to append." -Continue + } + else { + Stop-Function -Message "$Destination exists. Use -AllowClobber to overwrite." -Continue + } + } + } + + if ((Test-Path "$Destination.$type") -and -not $Append) { + if ($AllowClobber) { + try { + Remove-Item -Path "$Destination.$type" -ErrorAction Stop + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Continue + } + } + else { + if ($Type -eq "bin") { + Stop-Function -Message "$("$Destination.$type") exists. Use -AllowClobber to overwrite or -Append to append." -Continue + } + else { + Stop-Function -Message "$("$Destination.$type") exists. Use -AllowClobber to overwrite." -Continue + } + } + } + } + } + + $arguments = ($params -join " ") + + try { + if ($Raw) { + Write-Message -Level Output -Message "relog $arguments" + cmd /c "relog $arguments" + } + else { + Write-Message -Level Verbose -Message "relog $arguments" + $scriptblock = { + $output = (cmd /c "relog $arguments" | Out-String).Trim() + + if ($output -notmatch "Success") { + Stop-Function -Continue -Message $output.Trim("Input") + } + else { + Write-Message -Level Verbose -Message "$output" + $array = $output -Split [environment]::NewLine + $files = $array | Select-String "File:" + + foreach ($rawfile in $files) { + $rawfile = $rawfile.ToString().Replace("File:", "").Trim() + $gcierror = $null + Get-ChildItem $rawfile -ErrorAction SilentlyContinue -ErrorVariable gcierror | Add-Member -MemberType NoteProperty -Name RelogFile -Value $true -PassThru -ErrorAction Ignore + if ($gcierror) { + Write-Message -Level Verbose -Message "$gcierror" + } + } + } + } + Invoke-Command -ScriptBlock $scriptblock + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $path + } + } + + if ($Multithread) { + $allpaths | Invoke-Parallel -ImportVariables -ImportModules -ScriptBlock $scriptblock -ErrorAction SilentlyContinue -ErrorVariable parallelerror + if ($parallelerror) { + Write-Message -Level Verbose -Message "$parallelerror" + } + } + else { + foreach ($file in $allpaths) { Invoke-Command -ScriptBlock $scriptblock -ArgumentList $file } + } + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaSqlQuery.ps1 b/functions/Invoke-DbaSqlQuery.ps1 new file mode 100644 index 000000000000..2b507f5bc07c --- /dev/null +++ b/functions/Invoke-DbaSqlQuery.ps1 @@ -0,0 +1,347 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Invoke-DbaSqlQuery { + <# + .SYNOPSIS + A command to run explicit T-SQL commands or files. + + .DESCRIPTION + This function is a wrapper command around Invoke-DbaSqlAsync, which in turn is based on Invoke-SqlCmd2. + It was designed to be more convenient to use in a pipeline and to behave in a way consistent with the rest of our functions. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server Instance as a different user. This can be a Windows or SQL Server account. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Database + The database to select before running the query. This list is auto-populated from the server. + + .PARAMETER Query + Specifies one or more queries to be run. The queries can be Transact-SQL, XQuery statements, or sqlcmd commands. Multiple queries in a single batch may be separated by a semicolon or a GO + + Escape any double quotation marks included in the string. + + Consider using bracketed identifiers such as [MyTable] instead of quoted identifiers such as "MyTable". + + .PARAMETER QueryTimeout + Specifies the number of seconds before the queries time out. + + .PARAMETER File + Specifies the path to one or several files to be used as the query input. + + .PARAMETER SqlObject + Specify on or multiple SQL objects. Those will be converted to script and their scripts run on the target system(s). + + .PARAMETER As + Specifies output type. Valid options for this parameter are 'DataSet', 'DataTable', 'DataRow', 'PSObject', and 'SingleValue' + + PSObject output introduces overhead but adds flexibility for working with results: http://powershell.org/wp/forums/topic/dealing-with-dbnull/ + + .PARAMETER SqlParameters + Specifies a hashtable of parameters for parameterized SQL queries. http://blog.codinghorror.com/give-me-parameterized-sql-or-give-me-death/ + + .PARAMETER AppendServerInstance + If this switch is enabled, the SQL Server instance will be appended to PSObject and DataRow output. + + .PARAMETER MessagesToOutput + Use this switch to have on the output stream messages too (e.g. PRINT statements). Output will hold the resultset too. See examples for detail + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Query + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaSqlQuery + + .EXAMPLE + Invoke-DbaSqlQuery -SqlInstance server\instance -Query 'SELECT foo FROM bar' + + Runs the sql query 'SELECT foo FROM bar' against the instance 'server\instance' + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance [SERVERNAME] -Group [GROUPNAME] | Invoke-DbaSqlQuery -Query 'SELECT foo FROM bar' + + Runs the sql query 'SELECT foo FROM bar' against all instances in the group [GROUPNAME] on the CMS [SERVERNAME] + + .EXAMPLE + "server1", "server1\nordwind", "server2" | Invoke-DbaSqlQuery -File "C:\scripts\sql\rebuild.sql" + + Runs the sql commands stored in rebuild.sql against the instances "server1", "server1\nordwind" and "server2" + + .EXAMPLE + Get-DbaDatabase -SqlInstance "server1", "server1\nordwind", "server2" | Invoke-DbaSqlQuery -File "C:\scripts\sql\rebuild.sql" + + Runs the sql commands stored in rebuild.sql against all accessible databases of the instances "server1", "server1\nordwind" and "server2" + #> + [CmdletBinding(DefaultParameterSetName = "Query")] + Param ( + [parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]] + $SqlInstance, + + [Alias("Credential")] + [PsCredential] + $SqlCredential, + + [object]$Database, + + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Query")] + [string] + $Query, + + [Int32] + $QueryTimeout = 600, + + [Parameter(Mandatory = $true, ParameterSetName = "File")] + [object[]] + $File, + + [Parameter(Mandatory = $true, ParameterSetName = "SMO")] + [Microsoft.SqlServer.Management.Smo.SqlSmoObject[]] + $SqlObject, + + [ValidateSet("DataSet", "DataTable", "DataRow", "PSObject", "SingleValue")] + [string] + $As = "DataRow", + + [System.Collections.IDictionary] + $SqlParameters, + + [switch] + $AppendServerInstance, + + [switch] + $MessagesToOutput, + + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + + [Alias('Silent')] + [switch] + $EnableException + + ) + + begin { + Write-Message -Level Debug -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $splatInvokeDbaSqlAsync = @{ + As = $As + } + + if (Test-Bound -ParameterName "SqlParameters") { + $splatInvokeDbaSqlAsync["SqlParameters"] = $SqlParameters + } + if (Test-Bound -ParameterName "AppendServerInstance") { + $splatInvokeDbaSqlAsync["AppendServerInstance"] = $AppendServerInstance + } + if (Test-Bound -ParameterName "Query") { + $splatInvokeDbaSqlAsync["Query"] = $Query + } + if (Test-Bound -ParameterName "QueryTimeout") { + $splatInvokeDbaSqlAsync["QueryTimeout"] = $QueryTimeout + } + if (Test-Bound -ParameterName "MessagesToOutput") { + $splatInvokeDbaSqlAsync["MessagesToOutput"] = $MessagesToOutput + } + if (Test-Bound -ParameterName "Verbose") { + $splatInvokeDbaSqlAsync["Verbose"] = $Verbose + } + + + if (Test-Bound -ParameterName "File") { + $files = @() + $temporaryFiles = @() + $temporaryFilesCount = 0 + $temporaryFilesPrefix = (97 .. 122 | Get-Random -Count 10 | ForEach-Object { [char]$_ }) -join '' + + foreach ($item in $File) { + if ($null -eq $item) { continue } + + $type = $item.GetType().FullName + + switch ($type) { + "System.IO.DirectoryInfo" { + if (-not $item.Exists) { + Stop-Function -Message "Directory not found!" -Category ObjectNotFound + return + } + $files += ($item.GetFiles() | Where-Object Extension -EQ ".sql").FullName + + } + "System.IO.FileInfo" { + if (-not $item.Exists) { + Stop-Function -Message "Directory not found!" -Category ObjectNotFound + return + } + + $files += $item.FullName + } + "System.String" { + $uri = [uri]$item + + switch -regex ($uri.Scheme) { + "http" { + $tempfile = "$env:TEMP\$temporaryFilesPrefix-$temporaryFilesCount.sql" + try { + Invoke-WebRequest -Uri $item -OutFile $tempfile -ErrorAction Stop + $files += $tempfile + $temporaryFilesCount++ + $temporaryFiles += $tempfile + } + catch { + Stop-Function -Message "Failed to download file $item" -ErrorRecord $_ + return + } + } + default { + try { + $paths = Resolve-Path $item | Select-Object -ExpandProperty Path | Get-Item -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to resolve path: $item" -ErrorRecord $_ + return + } + + foreach ($path in $paths) { + if (-not $path.PSIsContainer) { + if (([uri]$path.FullName).Scheme -ne 'file') { + Stop-Function -Message "Could not resolve path $path as filesystem object" + return + } + $files += $path.FullName + } + } + } + } + } + default { + Stop-Function -Message "Unkown input type: $type" -Category InvalidArgument + return + } + } + } + } + + if (Test-Bound -ParameterName "SqlObject") { + $files = @() + $temporaryFiles = @() + $temporaryFilesCount = 0 + $temporaryFilesPrefix = (97 .. 122 | Get-Random -Count 10 | ForEach-Object { [char]$_ }) -join '' + + foreach ($object in $SqlObject) { + try { $code = Export-DbaScript -InputObject $object -Passthru -EnableException } + catch { + Stop-Function -Message "Failed to generate script for object $object" -ErrorRecord $_ + return + } + + try { + $newfile = "$env:TEMP\$temporaryFilesPrefix-$temporaryFilesCount.sql" + Set-Content -Value $code -Path $newfile -Force -ErrorAction Stop -Encoding UTF8 + $files += $newfile + $temporaryFilesCount++ + $temporaryFiles += $newfile + } + catch { + Stop-Function -Message "Failed to write sql script to temp" -ErrorRecord $_ + return + } + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + if (Test-Bound -ParameterName "Database", "InputObject" -And) { + Stop-Function -Category InvalidArgument -Message "You can't use -Database with piped databases" + return + } + if (Test-Bound -ParameterName "SqlInstance", "InputObject" -And) { + Stop-Function -Category InvalidArgument -Message "You can't use -SqlInstance with piped databases" + return + } + + foreach ($db in $InputObject) { + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $db is not accessible. Skipping." + continue + } + $server = $db.Parent + $conncontext = $server.ConnectionContext + if ($conncontext.DatabaseName -ne $db.Name) { + $conncontext = $server.ConnectionContext.Copy() + $conncontext.DatabaseName = $db.Name + } + try { + if ($File -or $SqlObject) { + foreach ($item in $files) { + if ($null -eq $item) {continue} + $filePath = $(Resolve-Path -LiteralPath $item).ProviderPath + $QueryfromFile = [System.IO.File]::ReadAllText("$filePath") + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync -Query $QueryfromFile + } + } + else { Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync } + } + catch { + Stop-Function -Message "[$db] Failed during execution" -ErrorRecord $_ -Target $server -Continue + } + } + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $instance -Continue + } + $conncontext = $server.ConnectionContext + try { + if ($Database -and $conncontext.DatabaseName -ne $Database) { + $conncontext = $server.ConnectionContext.Copy() + $conncontext.DatabaseName = $Database + } + if ($File -or $SqlObject) { + foreach ($item in $files) { + if ($null -eq $item) {continue} + $filePath = $(Resolve-Path -LiteralPath $item).ProviderPath + $QueryfromFile = [System.IO.File]::ReadAllText("$filePath") + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync -Query $QueryfromFile + } + } + else { + Invoke-DbaSqlAsync -SQLConnection $conncontext @splatInvokeDbaSqlAsync + } + } + catch { + Stop-Function -Message "[$instance] Failed during execution" -ErrorRecord $_ -Target $instance -Continue + } + } + } + + end { + # Execute end even when interrupting, as only used for cleanup + + if ($temporaryFiles) { + # Clean up temporary files that were downloaded + foreach ($item in $temporaryFiles) { + Remove-Item -Path $item -ErrorAction Ignore + } + } + Test-DbaDeprecation -DeprecatedOn '1.0.0' -Alias Invoke-DbaSqlCmd + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaWhoisActive.ps1 b/functions/Invoke-DbaWhoisActive.ps1 new file mode 100644 index 000000000000..0c1d65bc4302 --- /dev/null +++ b/functions/Invoke-DbaWhoisActive.ps1 @@ -0,0 +1,346 @@ +function Invoke-DbaWhoIsActive { + <# + .SYNOPSIS + Outputs results of Adam Machanic's sp_WhoIsActive DataTable + + .DESCRIPTION + Output results of Adam Machanic's sp_WhoIsActive + + This command was built with Adam's permission. To read more about sp_WhoIsActive, please visit: + + Updates: http://sqlblog.com/blogs/adam_machanic/archive/tags/who+is+active/default.aspx + + Also, consider donating to Adam if you find this stored procedure helpful: http://tinyurl.com/WhoIsActiveDonate + + .PARAMETER SqlInstance + The SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER Database + The database where sp_WhoIsActive is installed. Defaults to master. If the sp_WhoIsActive is not installed, the command will warn and exit. + + .PARAMETER Filter + FiltersBoth inclusive and exclusive + Set either filter to '' to disable + Session is a session ID, and either 0 or '' can be used to indicate "all" sessions + All other filter types support % or _ as wildcards + + .PARAMETER FilterType + Valid filter types are: session, program, database, login, and host + + .PARAMETER NotFilter + FiltersBoth inclusive and exclusive + Set either filter to '' to disable + Session is a session ID, and either 0 or '' can be used to indicate "all" sessions + All other filter types support % or _ as wildcards + + .PARAMETER NotFilterType + Valid filter types are: session, program, database, login, and host + + .PARAMETER ShowOwnSpid + Retrieve data about the calling session? + + .PARAMETER ShowSystemSpids + Retrieve data about system sessions? + + .PARAMETER ShowSleepingSpids + Controls how sleeping SPIDs are handled, based on the idea of levels of interest + 0 does not pull any sleeping SPIDs + 1 pulls only those sleeping SPIDs that also have an open transaction + 2 pulls all sleeping SPIDs + + .PARAMETER GetFullInnerText + If 1, gets the full stored procedure or running batch, when available + If 0, gets only the actual statement that is currently running in the batch or procedure + + .PARAMETER GetPlans + Get associated query plans for running tasks, if available + If 1, gets the plan based on the request's statement offset + If 2, gets the entire plan based on the request's plan_handle + + .PARAMETER GetOuterCommand + Get the associated outer ad hoc query or stored procedure call, if available + + .PARAMETER GetTransactionInfo + Enables pulling transaction log write info and transaction duration + + .PARAMETER GetTaskInfo + Get information on active tasks, based on three interest levels + Level 0 does not pull any task-related information + Level 1 is a lightweight mode that pulls the top non-CXPACKET wait, giving preference to blockers + Level 2 pulls all available task-based metrics, including: + number of active tasks, current wait stats, physical I/O, context switches, and blocker information + + .PARAMETER GetLocks + Gets associated locks for each request, aggregated in an XML format + + .PARAMETER GetAverageTime + Get average time for past runs of an active query + (based on the combination of plan handle, sql handle, and offset) + + .PARAMETER GetAdditonalInfo + Get additional non-performance-related information about the session or request + text_size, language, date_format, date_first, quoted_identifier, arithabort, ansi_null_dflt_on, + ansi_defaults, ansi_warnings, ansi_padding, ansi_nulls, concat_null_yields_null, + transaction_isolation_level, lock_timeout, deadlock_priority, row_count, command_type + + If a SQL Agent job is running, an subnode called agent_info will be populated with some or all of + the following: job_id, job_name, step_id, step_name, msdb_query_error (in the event of an error) + + If @get_task_info is set to 2 and a lock wait is detected, a subnode called block_info will be + populated with some or all of the following: lock_type, database_name, object_id, file_id, hobt_id, + applock_hash, metadata_resource, metadata_class_id, object_name, schema_name + + .PARAMETER FindBlockLeaders + Walk the blocking chain and count the number of + total SPIDs blocked all the way down by a given session + Also enables task_info Level 1, if @get_task_info is set to 0 + + .PARAMETER DeltaInterval + Pull deltas on various metrics + Interval in seconds to wait before doing the second data pull + + .PARAMETER OutputColumnList + List of desired output columns, in desired order + Note that the final output will be the intersection of all enabled features and all + columns in the list. Therefore, only columns associated with enabled features will + actually appear in the output. Likewise, removing columns from this list may effectively + disable features, even if they are turned on + + Each element in this list must be one of the valid output column names. Names must be + delimited by square brackets. White space, formatting, and additional characters are + allowed, as long as the list contains exact matches of delimited valid column names. + + .PARAMETER SortOrder + Column(s) by which to sort output, optionally with sort directions. + Valid column choices: + session_id, physical_io, reads, physical_reads, writes, tempdb_allocations, + tempdb_current, CPU, context_switches, used_memory, physical_io_delta, + reads_delta, physical_reads_delta, writes_delta, tempdb_allocations_delta, + tempdb_current_delta, CPU_delta, context_switches_delta, used_memory_delta, + tasks, tran_start_time, open_tran_count, blocking_session_id, blocked_session_count, + percent_complete, host_name, login_name, database_name, start_time, login_time + + Note that column names in the list must be bracket-delimited. Commas and/or white + space are not required. + + .PARAMETER FormatOutput + Formats some of the output columns in a more "human readable" form + 0 disables output format + 1 formats the output for variable-width fonts + 2 formats the output for fixed-width fonts + + .PARAMETER DestinationTable + If set to a non-blank value, the script will attempt to insert into the specified destination table. Please note that the script will not verify that the table exists, or that it has the correct schema, before doing the insert. Table can be specified in one, two, or three-part format + + .PARAMETER ReturnSchema + If set to 1, no data collection will happen and no result set will be returned; instead, + a CREATE TABLE statement will be returned via the @schema parameter, which will match + the schema of the result set that would be returned by using the same collection of the + rest of the parameters. The CREATE TABLE statement will have a placeholder token of + in place of an actual table name. + + .PARAMETER Schema + If set to 1, no data collection will happen and no result set will be returned; instead, + a CREATE TABLE statement will be returned via the @schema parameter, which will match + the schema of the result set that would be returned by using the same collection of the + rest of the parameters. The CREATE TABLE statement will have a placeholder token of + in place of an actual table name. + + .PARAMETER Help + Help! What do I do? + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: AdamMechanic, WhoIsActive, SpWhoIsActive + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Invoke-DbaWhoIsActive + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a + + Execute sp_whoisactive on sqlserver2014a. This command expects sp_WhoIsActive to be in the master database. Logs into the SQL Server with Windows credentials. + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -SqlCredential $credential -Database dbatools + + Execute sp_whoisactive on sqlserver2014a. This command expects sp_WhoIsActive to be in the dbatools database. Logs into the SQL Server with SQL Authentication. + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -GetAverageTime + + Similar to running sp_WhoIsActive @get_avg_time + + .EXAMPLE + Invoke-DbaWhoIsActive -SqlInstance sqlserver2014a -GetOuterCommand -FindBlockLeaders + + Similar to running sp_WhoIsActive @get_outer_command = 1, @find_block_leaders = 1 + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ServerInstance', 'SqlServer')] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [object]$Database, + [Alias('As')] + [ValidateLength(0, 128)] + [string]$Filter, + [ValidateSet('Session', 'Program', 'Database', 'Login', 'Host')] + [string]$FilterType = 'Session', + [ValidateLength(0, 128)] + [string]$NotFilter, + [ValidateSet('Session', 'Program', 'Database', 'Login', 'Host')] + [string]$NotFilterType = 'Session', + [switch]$ShowOwnSpid, + [switch]$ShowSystemSpids, + [ValidateRange(0, 255)] + [int]$ShowSleepingSpids, + [switch]$GetFullInnerText, + [ValidateRange(0, 255)] + [int]$GetPlans, + [switch]$GetOuterCommand, + [switch]$GetTransactionInfo, + [ValidateRange(0, 2)] + [int]$GetTaskInfo, + [switch]$GetLocks, + [switch]$GetAverageTime, + [switch]$GetAdditonalInfo, + [switch]$FindBlockLeaders, + [ValidateRange(0, 255)] + [int]$DeltaInterval, + [ValidateLength(0, 8000)] + [string]$OutputColumnList = '[dd%][session_id][sql_text][sql_command][login_name][wait_info][tasks][tran_log%][cpu%][temp%][block%][reads%][writes%][context%][physical%][query_plan][locks][%]', + [ValidateLength(0, 500)] + [string]$SortOrder = '[start_time] ASC', + [ValidateRange(0, 255)] + [int]$FormatOutput = 1, + [ValidateLength(0, 4000)] + [string]$DestinationTable = '', + [switch]$ReturnSchema, + [string]$Schema, + [switch]$Help, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $passedparams = $psboundparameters.Keys | Where-Object { 'Silent', 'SqlServer', 'SqlCredential', 'OutputAs', 'ServerInstance', 'SqlInstance', 'Database' -notcontains $_ } + $localparams = $psboundparameters + } + + process { + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.VersionMajor -lt 9) { + throw "sp_WhoIsActive is only supported in SQL Server 2005 and above" + } + + $paramdictionary = @{ + Filter = '@filter' + FilterType = '@filter_type' + NotFilter = 'not_filter' + NotFilterType = '@not_filter_type' + ShowOwnSpid = '@show_own_spid' + ShowSystemSpids = '@show_system_spids' + ShowSleepingSpids = '@show_sleeping_spids' + GetFullInnerText = '@get_full_inner_text' + GetPlans = '@get_plans' + GetOuterCommand = '@get_outer_command' + GetTransactionInfo = '@get_transaction_info' + GetTaskInfo = '@get_task_info' + GetLocks = '@get_locks ' + GetAverageTime = '@get_avg_time' + GetAdditonalInfo = '@get_additional_info' + FindBlockLeaders = '@find_block_leaders' + DeltaInterval = '@delta_interval' + OutputColumnList = '@output_column_list' + SortOrder = '@sort_order' + FormatOutput = '@format_output ' + DestinationTable = '@destination_table ' + ReturnSchema = '@return_schema' + Schema = '@schema' + Help = '@help' + } + + Write-Message -Level Verbose -Message "Collecting sp_whoisactive data from server: $instance" + + try { + $sqlconnection = New-Object System.Data.SqlClient.SqlConnection + $sqlconnection.ConnectionString = $server.ConnectionContext.ConnectionString + $sqlconnection.Open() + + if ($Database) { + # database is being returned as something weird. change it to string without using a method then trim. + $Database = "$Database" + $Database = $Database.Trim() + $sqlconnection.ChangeDatabase($Database) + } + + $sqlcommand = New-Object System.Data.SqlClient.SqlCommand + $sqlcommand.CommandType = "StoredProcedure" + $sqlcommand.CommandText = "dbo.sp_WhoIsActive" + $sqlcommand.Connection = $sqlconnection + + foreach ($param in $passedparams) { + Write-Message -Level Verbose -Message "Check parameter '$param'" + + $sqlparam = $paramdictionary[$param] + + if ($sqlparam) { + + $value = $localparams[$param] + + switch ($value) { + $true { $value = 1 } + $false { $value = 0 } + } + Write-Message -Level Verbose -Message "Adding parameter '$sqlparam' with value '$value'" + [Void]$sqlcommand.Parameters.AddWithValue($sqlparam, $value) + } + } + + $datatable = New-Object system.Data.DataSet + $dataadapter = New-Object system.Data.SqlClient.SqlDataAdapter($sqlcommand) + $dataadapter.fill($datatable) | Out-Null + $datatable.Tables.Rows + } + catch { + if ($_.Exception.InnerException -Like "*Could not find*") { + Stop-Function -Message "sp_whoisactive not found, please install using Install-DbaWhoIsActive." -Continue + } + else { + Stop-Function -Message "Invalid query." -Continue + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Show-SqlWhoIsActive -CustomMessage "Show-SqlWhoIsActive is no longer supported. Use Invoke-DbaWhoIsActive | Out-GridView for similar results." + } +} \ No newline at end of file diff --git a/functions/Invoke-DbaXEReplay.ps1 b/functions/Invoke-DbaXEReplay.ps1 new file mode 100644 index 000000000000..4cee9822accb --- /dev/null +++ b/functions/Invoke-DbaXEReplay.ps1 @@ -0,0 +1,140 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Invoke-DbaXeReplay { + <# + .SYNOPSIS + This command replays events from Read-DbaXEFile on one or more target servers + + .DESCRIPTION + This command replays events from Read-DbaXEFile. It is simplistic in its approach. + + - Writes all queries to a temp sql file + - Executes temp file using . $sqlcmd so that batches are executed properly + - Deletes temp file + + .PARAMETER SqlInstance + Target SQL Server(s) + + .PARAMETER SqlCredential + Used to provide alternative credentials. + + .PARAMETER Database + The initial starting database. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Raw + By dafault, the results of . $sqlcmd are collected, cleaned up and displayed. If you'd like to see all results immeidately, use Raw. + + .PARAMETER InputObject + Accepts the object output of Read-DbaXESession. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017 + + Runs all batch_text for sql_batch_completed against tempdb on sql2017. + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017 -Database planning -Event sql_batch_completed + + Sets the *initial* database to planning then runs only sql_batch_completed against sql2017. + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\sample.xel | Invoke-DbaXeReplay -SqlInstance sql2017, sql2016 + + Runs all batch_text for sql_batch_completed against tempdb on sql2017 and sql2016. + + #> + Param ( + [Parameter(Mandatory)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PsCredential]$SqlCredential, + [string[]]$Database, + [string[]]$Event = @('sql_batch_completed', 'rcp_completed'), + [Parameter(Mandatory, ValueFromPipeline)] + [object]$InputObject, + [switch]$Raw, + [switch]$EnableException + ) + + begin { + $querycolumns = 'statement', 'batch_text' + $timestamp = (Get-Date -Format yyyyMMddHHmm) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $filename = "$temp\dbatools-replay-$timestamp.sql" + Set-Content $filename -Value $null + + $sqlcmd = "$script:PSModuleRoot\bin\sqlcmd\sqlcmd.exe" + } + process { + if (Test-FunctionInterrupt) { return } + if ($InputObject.Name -notin $Event) { + continue + } + + if ($InputObject.statement) { + if ($InputObject.statement -notmatch "ALTER EVENT SESSION") { + Add-Content -Path $filename -Value $InputObject.statement + Add-Content -Path $filename -Value "GO" + } + } + else { + if ($InputObject.batch_text -notmatch "ALTER EVENT SESSION") { + Add-Content -Path $filename -Value $InputObject.batch_text + Add-Content -Path $filename -Value "GO" + } + } + } + end { + if (Test-FunctionInterrupt) { return } + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance." -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $instance -Continue + } + + + if ($Raw) { + if (Test-Bound -ParameterName SqlCredential) { + . $sqlcmd -S $instance -i $filename -U $SqlCredential.Username -P $SqlCredential.GetNetworkCredential().Password + continue + } + else { + . $sqlcmd -S $instance -i $filename + continue + } + } + + if (Test-Bound -ParameterName SqlCredential) { + $output = . $sqlcmd -S $instance -i $filename -U $SqlCredential.Username -P $SqlCredential.GetNetworkCredential().Password + } + else { + $output = . $sqlcmd -S $instance -i $filename + } + + foreach ($line in $output) { + $newline = $line.Trim() + if ($newline -and $newline -notmatch "------------------------------------------------------------------------------------") { + "$newline" + } + } + } + Remove-Item -Path $filename -ErrorAction Ignore + } +} \ No newline at end of file diff --git a/functions/Invoke-SqlCmd2.ps1 b/functions/Invoke-SqlCmd2.ps1 new file mode 100644 index 000000000000..82a8ddc28f40 --- /dev/null +++ b/functions/Invoke-SqlCmd2.ps1 @@ -0,0 +1,564 @@ +function Invoke-Sqlcmd2 { + <# + .SYNOPSIS + Runs a T-SQL script. + + .DESCRIPTION + Runs a T-SQL script. Invoke-Sqlcmd2 runs the whole script and only captures the first selected result set, such as the output of PRINT statements when -verbose parameter is specified. + Parameterized queries are supported. + + Help details below borrowed from Invoke-Sqlcmd + + .PARAMETER ServerInstance + Specifies the SQL Server instance(s) to execute the query against. + + .PARAMETER Database + Specifies the name of the database to execute the query against. If specified, this database will be used in the ConnectionString when establishing the connection to SQL Server. + + If a SQLConnection is provided, the default database for that connection is overridden with this database. + + .PARAMETER Query + Specifies one or more queries to be run. The queries can be Transact-SQL, XQuery statements, or sqlcmd commands. Multiple queries in a single batch may be separated by a semicolon. + + Do not specify the sqlcmd GO separator (or, use the ParseGo parameter). Escape any double quotation marks included in the string. + + Consider using bracketed identifiers such as [MyTable] instead of quoted identifiers such as "MyTable". + + .PARAMETER InputFile + Specifies the full path to a file to be used as the query input to Invoke-Sqlcmd2. The file can contain Transact-SQL statements, XQuery statements, sqlcmd commands and scripting variables. + + .PARAMETER Credential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + SECURITY NOTE: If you use the -Debug switch, the connectionstring including plain text password will be sent to the debug stream. + + .PARAMETER Encrypt + If this switch is enabled, the connection to SQL Server will be made using SSL. + + This requires that the SQL Server has been set up to accept SSL requests. For information regarding setting up SSL on SQL Server, see https://technet.microsoft.com/en-us/library/ms189067(v=sql.105).aspx + + .PARAMETER QueryTimeout + Specifies the number of seconds before the queries time out. + + .PARAMETER ConnectionTimeout + Specifies the number of seconds before Invoke-Sqlcmd2 times out if it cannot successfully connect to an instance of the Database Engine. The timeout value must be an integer between 0 and 65534. If 0 is specified, connection attempts do not time out. + + .PARAMETER As + Specifies output type. Valid options for this parameter are 'DataSet', 'DataTable', 'DataRow', 'PSObject', and 'SingleValue' + + PSObject output introduces overhead but adds flexibility for working with results: http://powershell.org/wp/forums/topic/dealing-with-dbnull/ + + .PARAMETER SqlParameters + Specifies a hashtable of parameters for parameterized SQL queries. http://blog.codinghorror.com/give-me-parameterized-sql-or-give-me-death/ + + Example: + + .PARAMETER AppendServerInstance + If this switch is enabled, the SQL Server instance will be appended to PSObject and DataRow output. + + .PARAMETER ParseGo + If this switch is enabled, "GO" statements will be handled automatically. + Every "GO" will effectively run in a separate query, like if you issued multiple Invoke-SqlCmd2 commands. + "GO"s will be recognized if they are on a single line, as this covers + the 95% of the cases "GO" parsing is needed + Note: + Queries will always target that database, e.g. if you have this Query: + USE DATABASE [dbname] + GO + SELECT * from sys.tables + and you call it via + Invoke-SqlCmd2 -ServerInstance instance -Database msdb -Query ... + you'll get back tables from msdb, not dbname. + + + .PARAMETER SQLConnection + Specifies an existing SQLConnection object to use in connecting to SQL Server. If the connection is closed, an attempt will be made to open it. + + .INPUTS + None + You cannot pipe objects to Invoke-Sqlcmd2 + + .OUTPUTS + As PSObject: System.Management.Automation.PSCustomObject + As DataRow: System.Data.DataRow + As DataTable: System.Data.DataTable + As DataSet: System.Data.DataTableCollectionSystem.Data.DataSet + As SingleValue: Dependent on data type in first column. + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -Query "SELECT login_time AS 'StartTime' FROM sysprocesses WHERE spid = 1" + + Connects to a named instance of the Database Engine on a computer and runs a basic T-SQL query. + + StartTime + ----------- + 2010-08-12 21:21:03.593 + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -InputFile "C:\MyFolder\tsqlscript.sql" | Out-File -filePath "C:\MyFolder\tsqlscript.rpt" + + Reads a file containing T-SQL statements, runs the file, and writes the output to another file. + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance "MyComputer\MyInstance" -Query "PRINT 'hello world'" -Verbose + + Uses the PowerShell -Verbose parameter to return the message output of the PRINT command. + VERBOSE: hello world + + .EXAMPLE + Invoke-Sqlcmd2 -ServerInstance MyServer\MyInstance -Query "SELECT ServerName, VCNumCPU FROM tblServerInfo" -as PSObject | ?{$_.VCNumCPU -gt 8} + Invoke-Sqlcmd2 -ServerInstance MyServer\MyInstance -Query "SELECT ServerName, VCNumCPU FROM tblServerInfo" -as PSObject | ?{$_.VCNumCPU} + + This example uses the PSObject output type to allow more flexibility when working with results. + + If we used DataRow rather than PSObject, we would see the following behavior: + Each row where VCNumCPU does not exist would produce an error in the first example + Results would include rows where VCNumCPU has DBNull value in the second example + + .EXAMPLE + 'Instance1', 'Server1/Instance1', 'Server2' | Invoke-Sqlcmd2 -query "Sp_databases" -as psobject -AppendServerInstance + + This example lists databases for each instance. It includes a column for the ServerInstance in question. + DATABASE_NAME DATABASE_SIZE REMARKS ServerInstance + ------------- ------------- ------- -------------- + REDACTED 88320 Instance1 + master 17920 Instance1 + ... + msdb 618112 Server1/Instance1 + tempdb 563200 Server1/Instance1 + ... + OperationsManager 20480000 Server2 + + .EXAMPLE + #Construct a query using SQL parameters + $Query = "SELECT ServerName, VCServerClass, VCServerContact FROM tblServerInfo WHERE VCServerContact LIKE @VCServerContact AND VCServerClass LIKE @VCServerClass" + + #Run the query, specifying values for SQL parameters + Invoke-Sqlcmd2 -ServerInstance SomeServer\NamedInstance -Database ServerDB -query $query -SqlParameters @{ VCServerContact="%cookiemonster%"; VCServerClass="Prod" } + + ServerName VCServerClass VCServerContact + ---------- ------------- --------------- + SomeServer1 Prod cookiemonster, blah + SomeServer2 Prod cookiemonster + SomeServer3 Prod blah, cookiemonster + + .EXAMPLE + Invoke-Sqlcmd2 -SQLConnection $Conn -Query "SELECT login_time AS 'StartTime' FROM sysprocesses WHERE spid = 1" + + Uses an existing SQLConnection and runs a basic T-SQL query against it + + StartTime + ----------- + 2010-08-12 21:21:03.593 + + .EXAMPLE + Invoke-SqlCmd -SQLConnection $Conn -Query "SELECT ServerName FROM tblServerInfo WHERE ServerName LIKE @ServerName" -SqlParameters @{"ServerName = "c-is-hyperv-1"} + + Executes a parameterized query against the existing SQLConnection, with a collection of one parameter to be passed to the query when executed. + + .NOTES + Changelog moved to CHANGELOG.md: + + https://github.com/sqlcollaborative/Invoke-SqlCmd2/blob/master/CHANGELOG.md + + .LINK + https://github.com/sqlcollaborative/Invoke-SqlCmd2 + + .LINK + https://github.com/RamblingCookieMonster/PowerShell + + .FUNCTIONALITY + SQL + #> + + [CmdletBinding(DefaultParameterSetName = 'Ins-Que')] + [OutputType([System.Management.Automation.PSCustomObject], [System.Data.DataRow], [System.Data.DataTable], [System.Data.DataTableCollection], [System.Data.DataSet])] + param ( + [Parameter(ParameterSetName = 'Ins-Que', + Position = 0, + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + HelpMessage = 'SQL Server Instance required...')] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 0, + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + HelpMessage = 'SQL Server Instance required...')] + [Alias('Instance', 'Instances', 'ComputerName', 'Server', 'Servers', 'SqlInstance')] + [ValidateNotNullOrEmpty()] + [string[]]$ServerInstance, + [Parameter(Position = 1, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [string]$Database, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Que', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [string]$Query, + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Fil', + Position = 2, + Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateScript( { Test-Path -LiteralPath $_ })] + [string]$InputFile, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 3, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 3, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Alias('SqlCredential')] + [System.Management.Automation.PSCredential]$Credential, + [Parameter(ParameterSetName = 'Ins-Que', + Position = 4, + Mandatory = $false, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 4, + Mandatory = $false, + ValueFromRemainingArguments = $false)] + [switch]$Encrypt, + [Parameter(Position = 5, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Int32]$QueryTimeout = 600, + [Parameter(ParameterSetName = 'Ins-Fil', + Position = 6, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Ins-Que', + Position = 6, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [Int32]$ConnectionTimeout = 15, + [Parameter(Position = 7, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateSet("DataSet", "DataTable", "DataRow", "PSObject", "SingleValue")] + [string]$As = "DataRow", + [Parameter(Position = 8, + Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [System.Collections.IDictionary]$SqlParameters, + [Parameter(Position = 9, + Mandatory = $false)] + [switch]$AppendServerInstance, + [Parameter(Position = 10, + Mandatory = $false)] + [switch]$ParseGO, + [Parameter(ParameterSetName = 'Con-Que', + Position = 11, + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false, + ValueFromRemainingArguments = $false)] + [Parameter(ParameterSetName = 'Con-Fil', + Position = 11, + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false, + ValueFromRemainingArguments = $false)] + [Alias('Connection', 'Conn')] + [ValidateNotNullOrEmpty()] + [System.Data.SqlClient.SQLConnection]$SQLConnection + ) + + begin { + if ($InputFile) { + $filePath = $(Resolve-Path -LiteralPath $InputFile).ProviderPath + $Query = [System.IO.File]::ReadAllText("$filePath") + } + + Write-Verbose "Running Invoke-Sqlcmd2 with ParameterSet '$($PSCmdlet.ParameterSetName)'. Performing query '$Query'." + + if ($As -eq "PSObject") { + #This code scrubs DBNulls. Props to Dave Wyatt + $cSharp = @' + using System; + using System.Data; + using System.Management.Automation; + + public class DBNullScrubber + { + public static PSObject DataRowToPSObject(DataRow row) + { + PSObject psObject = new PSObject(); + + if (row != null && (row.RowState & DataRowState.Detached) != DataRowState.Detached) + { + foreach (DataColumn column in row.Table.Columns) + { + Object value = null; + if (!row.IsNull(column)) + { + value = row[column]; + } + + psObject.Properties.Add(new PSNoteProperty(column.ColumnName, value)); + } + } + + return psObject; + } + } +'@ + + try { + Add-Type -TypeDefinition $cSharp -ReferencedAssemblies 'System.Data', 'System.Xml' -ErrorAction stop + } + catch { + if (-not $_.ToString() -like "*The type name 'DBNullScrubber' already exists*") { + Write-Warning "Could not load DBNullScrubber. Defaulting to DataRow output: $_." + $As = "Datarow" + } + } + } + + #Handle existing connections + if ($PSBoundParameters.ContainsKey('SQLConnection')) { + if ($SQLConnection.State -notlike "Open") { + try { + Write-Verbose "Opening connection from '$($SQLConnection.State)' state." + $SQLConnection.Open() + } + catch { + throw $_ + } + } + + if ($Database -and $SQLConnection.Database -notlike $Database) { + try { + Write-Verbose "Changing SQLConnection database from '$($SQLConnection.Database)' to $Database." + $SQLConnection.ChangeDatabase($Database) + } + catch { + throw "Could not change Connection database '$($SQLConnection.Database)' to $Database`: $_" + } + } + + if ($SQLConnection.state -like "Open") { + $ServerInstance = @($SQLConnection.DataSource) + } + else { + throw "SQLConnection is not open" + } + } + $GoSplitterRegex = [regex]'(?smi)^[\s]*GO[\s]*$' + + } + process { + foreach ($SQLInstance in $ServerInstance) { + Write-Verbose "Querying ServerInstance '$SQLInstance'" + + if ($PSBoundParameters.Keys -contains "SQLConnection") { + $Conn = $SQLConnection + } + else { + $CSBuilder = New-Object -TypeName System.Data.SqlClient.SqlConnectionStringBuilder + $CSBuilder["Server"] = $SQLInstance + $CSBuilder["Database"] = $Database + $CSBuilder["Connection Timeout"] = $ConnectionTimeout + + if ($Encrypt) { + $CSBuilder["Encrypt"] = $true + } + + if ($Credential) { + $CSBuilder["Trusted_Connection"] = $false + $CSBuilder["User ID"] = $Credential.UserName + $CSBuilder["Password"] = $Credential.GetNetworkCredential().Password + } + else { + $CSBuilder["Integrated Security"] = $true + } + if ($ApplicationName) { + $CSBuilder["Application Name"] = $ApplicationName + } + else { + $ScriptName = (Get-PSCallStack)[-1].Command.ToString() + if ($ScriptName -ne "") { + $CSBuilder["Application Name"] = $ScriptName + } + } + $conn = New-Object -TypeName System.Data.SqlClient.SQLConnection + + $ConnectionString = $CSBuilder.ToString() + $conn.ConnectionString = $ConnectionString + Write-Debug "ConnectionString $ConnectionString" + + try { + $conn.Open() + } + catch { + Write-Error $_ + continue + } + } + + #Following EventHandler is used for PRINT and RAISERROR T-SQL statements. Executed when -Verbose parameter specified by caller + if ($PSBoundParameters.Verbose) { + $conn.FireInfoMessageEventOnUserErrors = $false # Shiyang, $true will change the SQL exception to information + $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { Write-Verbose "$($_)" } + $conn.add_InfoMessage($handler) + } + if ($ParseGO) { + Write-Verbose "Stripping GOs from source" + $Pieces = $GoSplitterRegex.Split($Query) + } + else { + $Pieces = , $Query + } + # Only execute non-empty statements + $Pieces = $Pieces | Where-Object { $_.Trim().Length -gt 0 } + foreach ($piece in $Pieces) { + $cmd = New-Object system.Data.SqlClient.SqlCommand($piece, $conn) + $cmd.CommandTimeout = $QueryTimeout + + if ($null -ne $SqlParameters) { + $SqlParameters.GetEnumerator() | + ForEach-Object { + if ($null -ne $_.Value) { + $cmd.Parameters.AddWithValue($_.Key, $_.Value) + } + else { + $cmd.Parameters.AddWithValue($_.Key, [DBNull]::Value) + } + } > $null + } + + $ds = New-Object system.Data.DataSet + $da = New-Object system.Data.SqlClient.SqlDataAdapter($cmd) + + try { + [void]$da.fill($ds) + } + catch [System.Data.SqlClient.SqlException] { + # For SQL exception + + $Err = $_ + + Write-Verbose "Capture SQL Error" + + if ($PSBoundParameters.Verbose) { + Write-Verbose "SQL Error: $Err" + } #Shiyang, add the verbose output of exception + + switch ($ErrorActionPreference.tostring()) { + { 'SilentlyContinue', 'Ignore' -contains $_ } { + + } + 'Stop' { + throw $Err + } + 'Continue' { + throw $Err + } + Default { + Throw $Err + } + } + } + catch { + # For other exception + Write-Verbose "Capture Other Error" + + $Err = $_ + + if ($PSBoundParameters.Verbose) { + Write-Verbose "Other Error: $Err" + } + + switch ($ErrorActionPreference.tostring()) { + { 'SilentlyContinue', 'Ignore' -contains $_ } { + + } + 'Stop' { + throw $Err + } + 'Continue' { + throw $Err + } + Default { + throw $Err + } + } + } + finally { + #Close the connection + if (-not $PSBoundParameters.ContainsKey('SQLConnection')) { + $conn.Close() + } + } + + if ($AppendServerInstance) { + #Basics from Chad Miller + $Column = New-Object Data.DataColumn + $Column.ColumnName = "ServerInstance" + + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0].Columns.Add($Column) + Foreach ($row in $ds.Tables[0]) { + $row.ServerInstance = $SQLInstance + } + } + } + + switch ($As) { + 'DataSet' { + $ds + } + 'DataTable' { + $ds.Tables + } + 'DataRow' { + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0] + } + } + 'PSObject' { + if ($ds.Tables.Count -ne 0) { + #Scrub DBNulls - Provides convenient results you can use comparisons with + #Introduces overhead (e.g. ~2000 rows w/ ~80 columns went from .15 Seconds to .65 Seconds - depending on your data could be much more!) + foreach ($row in $ds.Tables[0].Rows) { + + [DBNullScrubber]::DataRowToPSObject($row) + } + } + } + 'SingleValue' { + if ($ds.Tables.Count -ne 0) { + $ds.Tables[0] | Select-Object -ExpandProperty $ds.Tables[0].Columns[0].ColumnName + } + } + } + } + } + } +} #Invoke-Sqlcmd2 diff --git a/functions/Measure-DbaBackupThroughput.ps1 b/functions/Measure-DbaBackupThroughput.ps1 index d5157cf797c0..b8c37eecc6e7 100644 --- a/functions/Measure-DbaBackupThroughput.ps1 +++ b/functions/Measure-DbaBackupThroughput.ps1 @@ -1,185 +1,184 @@ -function Measure-DbaBackupThroughput -{ -<# -.SYNOPSIS -Determines how quickly SQL Server is backing up databases to media. - -.DESCRIPTION -Returns backup history details for some or all databases on a SQL Server. - -Output looks like this -SqlInstance : sql2016 -Database : SharePoint_Config -AvgThroughputMB : 1.07 -AvgSizeMB : 24.17 -AvgDuration : 00:00:01.1000000 -MinThroughputMB : 0.02 -MaxThroughputMB : 2.26 -MinBackupDate : 8/6/2015 10:22:01 PM -MaxBackupDate : 6/19/2016 12:57:45 PM -BackupCount : 10 - -.PARAMETER SqlInstance -SqlInstance name or SMO object representing the SQL Server to connect to. -This can be a collection and receive pipeline input. - -.PARAMETER SqlCredential -PSCredential object to connect as. If not specified, current Windows login will be used. - -.PARAMETER Type -By default, this command measures the speed of Full backups. You can also specify Log or Differential. - -.PARAMETER Since -Datetime object used to narrow the results to a date - -.PARAMETER Last -Measure only the last backup - - -.NOTES -Tags: DisasterRecovery, Backup -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see . -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Measure-DbaBackupThroughput - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2016 - -Parses every backup in msdb's backuphistory for stats on all databases - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2016 -Databases AdventureWorks2014 - -Parses every backup in msdb's backuphistory for stats on AdventureWorks2014 - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2005 -Last - -Processes the last full, diff and log backups every backup for all databases on sql2005 - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2005 -Last -Type Log - -Processes the last log backups every backup for all databases on sql2005 - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-7) - -Gets backup calculations for the last week - -.EXAMPLE -Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-365) -Databases bigoldb - -Gets backup calculations, limited to the last year and only the bigoldb database - -#> - [CmdletBinding()] - param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] - [Alias("ServerInstance", "Instance", "SqlServer")] - [object[]]$SqlInstance, - [PSCredential][System.Management.Automation.CredentialAttribute()]$SqlCredential, - [datetime]$Since, - [switch]$Last, - [ValidateSet("Full", "Log", "Differential")] - [string]$Type = "Full" - ) - - DynamicParam { if ($SqlInstance) { return Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $SqlCredential } } - - BEGIN - { - $databases = $psboundparameters.Databases - - if ($Since) - { - $Since = $Since.ToString("yyyy-MM-dd HH:mm:ss") - } - } - process - { - foreach ($instance in $SqlInstance) - { - try - { - Write-Verbose "Connecting to $instance" - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $sqlcredential - } - catch - { - Write-Warning "Failed to connect to $instance" - continue - } - - if (!$databases) { $databases = $server.databases.name } - - foreach ($database in $databases) - { - Write-Verbose "Getting backup history for $database" - - $allhistory = @() - - # Splatting didnt work - if ($since) - { - $histories = Get-DbaBackupHistory -SqlServer $server -Databases $database -Since $since | Where-Object Type -eq $Type - } - else - { - $histories = Get-DbaBackupHistory -SqlServer $server -Databases $database -Last:$last | Where-Object Type -eq $Type - } - - foreach ($history in $histories) - { - $timetaken = New-TimeSpan -Start $history.Start -End $history.End - - if ($timetaken.TotalMilliseconds -eq 0) - { - $throughput = $history.TotalSizeMB - } - else - { - $throughput = $history.TotalSizeMB % $timetaken.TotalSeconds + 1 - } - - Add-Member -InputObject $history -MemberType Noteproperty -Name MBps -value $throughput - - $allhistory += $history | Select-Object ComputerName, InstanceName, SqlInstance, Database, MBps, TotalSizeMB, Start, End - } - - foreach ($db in ($allhistory | Sort-Object Database | Group-Object Database)) - { - $measuremb = $db.Group.MBps | Measure-Object -Average -Minimum -Maximum - $measurestart = $db.Group.Start | Measure-Object -Minimum - $measureend = $db.Group.End | Measure-Object -Maximum - $measuresize = $db.Group.TotalSizeMB | Measure-Object -Average - $avgduration = $db.Group | ForEach-Object { New-TimeSpan -Start $_.Start -End $_.End } | Measure-Object -Average TotalSeconds - - $date = Get-Date - - [pscustomobject]@{ - ComputerName = $db.Group.ComputerName | Select-Object -First 1 - InstanceName = $db.Group.InstanceName | Select-Object -First 1 - SqlInstance = $db.Group.SqlInstance | Select-Object -First 1 - Database = $db.Name - AvgThroughputMB = [System.Math]::Round($measuremb.Average, 2) - AvgSizeMB = [System.Math]::Round($measuresize.Average, 2) - AvgDuration = New-TimeSpan -Start $date -End $date.AddSeconds($avgduration.Average) - MinThroughputMB = [System.Math]::Round($measuremb.Minimum, 2) - MaxThroughputMB = [System.Math]::Round($measuremb.Maximum, 2) - MinBackupDate = $measurestart.Minimum - MaxBackupDate = $measureend.Maximum - BackupCount = $db.Count - } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName - } - } - } - } +function Measure-DbaBackupThroughput { + <# + .SYNOPSIS + Determines how quickly SQL Server is backing up databases to media. + + .DESCRIPTION + Returns backup history details for one or more databases on a SQL Server. + + Output looks like this: + SqlInstance : sql2016 + Database : SharePoint_Config + AvgThroughputMB : 1.07 + AvgSizeMB : 24.17 + AvgDuration : 00:00:01.1000000 + MinThroughputMB : 0.02 + MaxThroughputMB : 2.26 + MinBackupDate : 8/6/2015 10:22:01 PM + MaxBackupDate : 6/19/2016 12:57:45 PM + BackupCount : 10 + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude. Options for this list are auto-populated from the server. + + .PARAMETER Type + By default, this command measures the speed of Full backups. Valid options are "Full", "Log" and "Differential". + + .PARAMETER Since + All backups taken on or after the point in time represented by this datetime object will be processed. + + .PARAMETER Last + If this switch is enabled, only the last backup will be measured. + + .PARAMETER DeviceType + Specifies one or more DeviceTypes to use in filtering backup sets. Valid values are "Disk", "Permanent Disk Device", "Tape", "Permanent Tape Device", "Pipe", "Permanent Pipe Device" and "Virtual Device", as well as custom integers for your own DeviceTypes. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup, Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Measure-DbaBackupThroughput + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 + + Parses every backup in msdb's backuphistory for stats on all databases. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Database AdventureWorks2014 + + Parses every backup in msdb's backuphistory for stats on AdventureWorks2014. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2005 -Last + + Processes the last full, diff and log backups every backup for all databases on sql2005. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2005 -Last -Type Log + + Processes the last log backups every backup for all databases on sql2005. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-7) + + Gets backup calculations for the last week. + + .EXAMPLE + Measure-DbaBackupThroughput -SqlInstance sql2016 -Since (Get-Date).AddDays(-365) -Database bigoldb + + Gets backup calculations, limited to the last year and only the bigoldb database + + #> + [CmdletBinding()] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "Instance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [datetime]$Since, + [switch]$Last, + [ValidateSet("Full", "Log", "Differential", "File", "Differential File", "Partial Full", "Partial Differential")] + [string]$Type = "Full", + [string[]]$DeviceType, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($Database) { + $DatabaseCollection = $server.Databases | Where-Object Name -in $Database + } + else { + $DatabaseCollection = $server.Databases + } + + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $DatabaseCollection) { + Write-Message -Level VeryVerbose -Message "Retrieving history for $db." + $allhistory = @() + + # Splatting didn't work + if ($since) { + $histories = Get-DbaBackupHistory -SqlInstance $server -Database $db.name -Since $since -DeviceType $DeviceType -Type $Type + } + else { + $histories = Get-DbaBackupHistory -SqlInstance $server -Database $db.name -Last:$last -DeviceType $DeviceType -Type $Type + } + + foreach ($history in $histories) { + $timetaken = New-TimeSpan -Start $history.Start -End $history.End + + if ($timetaken.TotalMilliseconds -eq 0) { + $throughput = $history.TotalSize.Megabyte + } + else { + $throughput = $history.TotalSize.Megabyte / $timetaken.TotalSeconds + } + + Add-Member -Force -InputObject $history -MemberType Noteproperty -Name MBps -value $throughput + + $allhistory += $history | Select-Object ComputerName, InstanceName, SqlInstance, Database, MBps, TotalSize, Start, End + } + + Write-Message -Level VeryVerbose -Message "Calculating averages for $db." + foreach ($db in ($allhistory | Sort-Object Database | Group-Object Database)) { + + $measuremb = $db.Group.MBps | Measure-Object -Average -Minimum -Maximum + $measurestart = $db.Group.Start | Measure-Object -Minimum + $measureend = $db.Group.End | Measure-Object -Maximum + $measuresize = $db.Group.TotalSize.Megabyte | Measure-Object -Average + $avgduration = $db.Group | ForEach-Object { New-TimeSpan -Start $_.Start -End $_.End } | Measure-Object -Average TotalSeconds + + [pscustomobject]@{ + ComputerName = $db.Group.ComputerName | Select-Object -First 1 + InstanceName = $db.Group.InstanceName | Select-Object -First 1 + SqlInstance = $db.Group.SqlInstance | Select-Object -First 1 + Database = $db.Name + AvgThroughputMB = [System.Math]::Round($measuremb.Average, 2) + AvgSizeMB = [System.Math]::Round($measuresize.Average, 2) + AvgDuration = [dbatimespan](New-TimeSpan -Seconds $avgduration.Average) + MinThroughputMB = [System.Math]::Round($measuremb.Minimum, 2) + MaxThroughputMB = [System.Math]::Round($measuremb.Maximum, 2) + MinBackupDate = [dbadatetime]$measurestart.Minimum + MaxBackupDate = [dbadatetime]$measureend.Maximum + BackupCount = $db.Count + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName + } + } + } + } } diff --git a/functions/Measure-DbaDiskSpaceRequirement.ps1 b/functions/Measure-DbaDiskSpaceRequirement.ps1 new file mode 100644 index 000000000000..035800db4ff3 --- /dev/null +++ b/functions/Measure-DbaDiskSpaceRequirement.ps1 @@ -0,0 +1,287 @@ +function Measure-DbaDiskSpaceRequirement { + <# + .SYNOPSIS + Calculate the space needed to copy and possibly replace a database from one SQL server to another. + + .DESCRIPTION + Returns a file list from source and destination where source file may overwrite destination. Complex scenarios where a new file may exist is taken into account. + This command will accept a hash object in pipeline with the following keys: Source, SourceDatabase, Destination. Using this command will provide a way to prepare before a complex migration with multiple databases from different sources and destinations. + + .PARAMETER Source + Source SQL Server. + + .PARAMETER SourceSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database to copy. It MUST exist. + + .PARAMETER Destination + Destination SQL Server instance. + + .PARAMETER DestinationSqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DestinationDatabase + The database name at destination. + May or may not be present, if unspecified it will default to the database name provided in SourceDatabase. + + .PARAMETER Credential + The credentials to use to connect via CIM/WMI/PowerShell remoting. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, DiskSpace, Migration + Author: Pollus Brodeur (@pollusb) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Measure-DbaDiskSpaceRequirement + + .EXAMPLE + Measure-DbaDiskSpaceRequirement -Source INSTANCE1 -Database DB1 -Destination INSTANCE2 + + Calculate space needed for a simple migration with one database with the same name at destination. + + .EXAMPLE + @([PSCustomObject]@{Source='SQL1';Destination='SQL2';Database='DB1'}, + [PSCustomObject]@{Source='SQL1';Destination='SQL2';Database='DB2'} + ) | Measure-DbaDiskSpaceRequirement + + Using a PSCustomObject with 2 databases to migrate on SQL2. + + .EXAMPLE + Import-Csv -Path .\migration.csv -Delimiter "`t" | Measure-DbaDiskSpaceRequirement | Format-Table -AutoSize + + Using a CSV file. You will need to use this header line "SourceDestinationDatabaseDestinationDatabase". + + .EXAMPLE + Invoke-DbaSqlCmd -SqlInstance DBA -Database Migrations -Query 'select Source, Destination, Database from dbo.Migrations' ` + | Measure-DbaDiskSpaceRequirement + + Using a SQL table. We are DBA after all! + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [DbaInstanceParameter]$Source, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string]$Database, + [PSCredential]$SourceSqlCredential, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [DbaInstanceParameter]$Destination, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [string]$DestinationDatabase, + [PSCredential]$DestinationSqlCredential, + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $local:cacheMP = @{} + $local:cacheDP = @{} + + function Get-MountPointFromPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $path, + [Parameter(Mandatory = $true)] + $computerName, + [PSCredential]$credential + ) + if (!$cacheMP[$computerName]) { + try { + $cacheMP.Add($computerName, (Get-DbaDiskSpace -ComputerName $computerName -Credential $Credential -EnableException)) + Write-Message -Level Verbose -Message "cacheMP[$computerName] is in cache" + } + catch { + # This way, I won't be asking again for this computer. + $cacheMP.Add($computerName, '?') + Stop-Function -Message "Can't connect to $computerName. cacheMP[$computerName] = ?" -ErrorRecord $_ -Target $computerName -Continue + } + } + if ($cacheMP[$computerName] -eq '?') { + return '?' + } + foreach ($m in ($cacheMP[$computerName] | Sort-Object -Property Name -Descending)) { + if ($path -like "$($m.Name)*") { + return $m.Name + } + } + Write-Message -Level Warning -Message "Path $path can't be found in any MountPoints of $computerName" + } + function Get-MountPointFromDefaultPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Log', 'Data')] + $DefaultPathType, + [Parameter(Mandatory = $true)] + $SqlInstance, + [PSCredential]$SqlCredential, + # Could probably use the computer defined in SqlInstance but info was already available from the caller + $computerName, + [PSCredential]$Credential + ) + if (!$cacheDP[$SqlInstance]) { + try { + $cacheDP.Add($SqlInstance, (Get-DbaDefaultPath -SqlInstance $SqlInstance -SqlCredential $SqlCredential -EnableException)) + Write-Message -Level Verbose -Message "cacheDP[$SqlInstance] is in cache" + } + catch { + Stop-Function -Message "Can't connect to $SqlInstance" -Continue + $cacheDP.Add($SqlInstance, '?') + return '?' + } + } + if ($cacheDP[$SqlInstance] -eq '?') { + return '?' + } + if (!$computerName) { + $computerName = $cacheDP[$SqlInstance].ComputerName + } + if (!$cacheMP[$computerName]) { + try { + $cacheMP.Add($computerName, (Get-DbaDiskSpace -ComputerName $computerName -Credential $Credential)) + } + catch { + Stop-Function -Message "Can't connect to $computerName." -Continue + $cacheMP.Add($computerName, '?') + return '?' + } + } + if ($DefaultPathType -eq 'Log') { + $path = $cacheDP[$SqlInstance].Log + } + else { + $path = $cacheDP[$SqlInstance].Data + } + foreach ($m in ($cacheMP[$computerName] | Sort-Object -Property Name -Descending)) { + if ($path -like "$($m.Name)*") { + return $m.Name + } + } + } + } + process { + Write-Message -Level Verbose -Message "Attempting to connect to SQL Servers." + try { + Write-Message -Level Verbose -Message "Connecting to $Source." + $sourceServer = Connect-SqlInstance -SqlInstance $Source -SqlCredential $SourceSqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $Source + } + + try { + Write-Message -Level Verbose -Message "Connecting to $Destination." + $destServer = Connect-SqlInstance -SqlInstance $Destination -SqlCredential $DestinationSqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $Destination + } + + if (Test-Bound 'DestinationDatabase' -not) { + $DestinationDatabase = $Database + } + Write-Message -Level Verbose -Message "$Source.[$Database] -> $Destination.[$DestinationDatabase]" + + $sourceDb = Get-DbaDatabase -SqlInstance $sourceServer -Database $Database -SqlCredential $SourceSqlCredential + if (Test-Bound 'Database' -not) { + Stop-Function -Message "Database [$Database] MUST exist on Source Instance $Source." -ErrorRecord $_ + } + $sourceFiles = @($sourceDb.FileGroups.Files | Select-Object Name, FileName, Size, @{n='Type'; e= {'Data'}}) + $sourceFiles += @($sourceDb.LogFiles | Select-Object Name, FileName, Size, @{n='Type'; e= {'Log'}}) + + if ($destDb = Get-DbaDatabase -SqlInstance $destServer -Database $DestinationDatabase -SqlCredential $DestinationSqlCredential) { + $destFiles = @($destDb.FileGroups.Files | Select-Object Name, FileName, Size, @{n='Type'; e= {'Data'}}) + $destFiles += @($destDb.LogFiles | Select-Object Name, FileName, Size, @{n='Type'; e= {'Log'}}) + $computerName = $destDb.ComputerName + } + else { + Write-Message -Level Verbose -Message "Database [$DestinationDatabase] does not exist on Destination Instance $Destination." + $computerName = $destServer.NetName + } + + foreach ($sourceFile in $sourceFiles) { + foreach ($destFile in $destFiles) { + if ($found = ($sourceFile.Name -eq $destFile.Name)) { + # Files found on both sides + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabase = $sourceDb.Name + SourceLogicalName = $sourceFile.Name + SourceFileName = $sourceFile.FileName + SourceFileSize = [DbaSize]($sourceFile.Size * 1000) + DestinationDatabase = $destDb.Name + DestinationLogicalName = $destFile.Name + DestinationFileName = $destFile.FileName + DestinationFileSize = [DbaSize]($destFile.Size * 1000) * -1 + DifferenceSize = [DbaSize]( ($sourceFile.Size * 1000) - ($destFile.Size * 1000) ) + MountPoint = Get-MountPointFromPath -Path $destFile.Filename -ComputerName $computerName -Credential $Credential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + break + } + } + if (!$found) { + # Files on source but not on destination + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabase = $sourceDb.Name + SourceLogicalName = $sourceFile.Name + SourceFileName = $sourceFile.FileName + SourceFileSize = [DbaSize]($sourceFile.Size * 1000) + DestinationDatabase = $DestinationDatabase + DestinationLogicalName = $null + DestinationFileName = $null + DestinationFileSize = [DbaSize]0 + DifferenceSize = [DbaSize]($sourceFile.Size * 1000) + MountPoint = Get-MountPointFromDefaultPath -DefaultPathType $sourceFile.Type -SqlInstance $destServer -SqlCredential $DestinationSqlCredential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + } + } + if ($destDb) { + # Files on destination but not on source (strange scenario but possible) + $destFilesNotSource = Compare-Object -ReferenceObject $destFiles -DifferenceObject $sourceFiles -Property Name -PassThru + foreach ($destFileNotSource in $destFilesNotSource) { + [PSCustomObject]@{ + SourceComputerName = $sourceServer.NetName + SourceInstance = $sourceServer.ServiceName + SourceSqlInstance = $sourceServer.DomainInstanceName + DestinationComputerName = $destServer.NetName + DestinationInstance = $destServer.ServiceName + DestinationSqlInstance = $destServer.DomainInstanceName + SourceDatabaseName = $Database + SourceLogicalName = $null + SourceFileName = $null + SourceFileSize = [DbaSize]0 + DestinationDatabaseName = $destDb.Name + DestinationLogicalName = $destFileNotSource.Name + DestinationFileName = $destFile.FileName + DestinationFileSize = [DbaSize]($destFileNotSource.Size * 1000) * -1 + DifferenceSize = [DbaSize]($destFileNotSource.Size * 1000) * -1 + MountPoint = Get-MountPointFromPath -Path $destFileNotSource.Filename -ComputerName $computerName -Credential $Credential + } | Select-DefaultView -ExcludeProperty SourceComputerName, SourceInstance, DestinationComputerName, DestinationInstance + } + } + $DestinationDatabase = $null + } +} diff --git a/functions/Mount-DbaDatabase.ps1 b/functions/Mount-DbaDatabase.ps1 new file mode 100644 index 000000000000..4bc5e393718f --- /dev/null +++ b/functions/Mount-DbaDatabase.ps1 @@ -0,0 +1,154 @@ +function Mount-DbaDatabase { + <# + .SYNOPSIS + Attach a SQL Server Database - aliased to Attach-DbaDatabase + + .DESCRIPTION + This command will attach a SQL Server database. + + .PARAMETER SqlInstance + The SQL Server instance. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to attach. + + .PARAMETER FileStructure + A StringCollection object value that contains a list database files. If FileStructure is not specified, BackupHistory will be used to guess the structure. + + .PARAMETER DatabaseOwner + Sets the database owner for the database. The sa account (or equivalent) will be used if DatabaseOwner is not specified. + + .PARAMETER AttachOption + An AttachOptions object value that contains the attachment options. Valid options are "None", "RebuildLog", "EnableBroker", "NewBroker" and "ErrorBrokerConversations". + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Mount-DbaDatabase + + .EXAMPLE + $fileStructure = New-Object System.Collections.Specialized.StringCollection + $fileStructure.Add("E:\archive\example.mdf") + $filestructure.Add("E:\archive\example.ldf") + $filestructure.Add("E:\archive\example.ndf") + Mount-DbaDatabase -SqlInstance sql2016 -Database example -FileStructure $fileStructure + + Attaches a database named "example" to sql2016 with the files "E:\archive\example.mdf", "E:\archive\example.ldf" and "E:\archive\example.ndf". The database owner will be set to sa and the attach option is None. + + .EXAMPLE + Mount-DbaDatabase -SqlInstance sql2016 -Database example + + Since the FileStructure was not provided, this command will attempt to determine it based on backup history. If found, a database named example will be attached to sql2016. + + .EXAMPLE + Mount-DbaDatabase -SqlInstance sql2016 -Database example -WhatIf + + Shows what would happen if the command were executed (without actually performing the command) + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential] + $SqlCredential, + [parameter(Mandatory)] + [string[]]$Database, + [System.Collections.Specialized.StringCollection]$FileStructure, + [string]$DatabaseOwner, + [ValidateSet('None', 'RebuildLog', 'EnableBroker', 'NewBroker', 'ErrorBrokerConversations')] + [string]$AttachOption = "None", + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (-not $server.Logins.Item($DatabaseOwner)) { + try { + $DatabaseOwner = ($server.Logins | Where-Object { $_.id -eq 1 }).Name + } + catch { + $DatabaseOwner = "sa" + } + } + + foreach ($db in $database) { + + if ($server.Databases[$db]) { + Stop-Function -Message "$db is already attached to $server." -Target $db -Continue + } + + if ($server.Databases[$db].IsSystemObject) { + Stop-Function -Message "$db is a system database and cannot be attached using this method." -Target $db -Continue + } + + if (-Not (Test-Bound -Parameter FileStructure)) { + $backuphistory = Get-DbaBackupHistory -SqlInstance $server -Database $db -Type Full | Sort-Object End -Descending | Select-Object -First 1 + + if (-not $backuphistory) { + $message = "Could not enumerate backup history to automatically build FileStructure. Rerun the command and provide the filestructure parameter." + Stop-Function -Message $message -Target $db -Continue + } + + $backupfile = $backuphistory.Path[0] + $filepaths = (Read-DbaBackupHeader -SqlInstance $server -FileList -Path $backupfile).PhysicalName + + $FileStructure = New-Object System.Collections.Specialized.StringCollection + foreach ($file in $filepaths) { + $exists = Test-DbaSqlpath -SqlInstance $server -Path $file + if (-not $exists) { + $message = "Could not find the files to build the FileStructure. Rerun the command and provide the FileStructure parameter." + Stop-Function -Message $message -Target $file -Continue + } + + $null = $FileStructure.Add($file) + } + } + + If ($Pscmdlet.ShouldProcess($server, "Attaching $Database with $DatabaseOwner as database owner and $AttachOption as attachoption")) { + try { + $server.AttachDatabase($db, $FileStructure, $DatabaseOwner, [Microsoft.SqlServer.Management.Smo.AttachOptions]::$AttachOption) + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + AttachResult = "Success" + AttachOption = $AttachOption + FileStructure = $FileStructure + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaAgentJob.ps1 b/functions/New-DbaAgentJob.ps1 new file mode 100644 index 000000000000..4d7f00500d8d --- /dev/null +++ b/functions/New-DbaAgentJob.ps1 @@ -0,0 +1,426 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentJob { + <# +.SYNOPSIS +New-DbaAgentJob creates a new job + +.DESCRIPTION +New-DbaAgentJob makes is possible to create a job in the SQL Server Agent. +It returns an array of the job(s) created + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. The name must be unique and cannot contain the percent (%) character. + +.PARAMETER Schedule +Schedule to attach to job. This can be more than one schedule. + +.PARAMETER ScheduleId +Schedule ID to attach to job. This can be more than one schedule ID. + +.PARAMETER Disabled +Sets the status of the job to disabled. By default a job is enabled. + +.PARAMETER Description +The description of the job. + +.PARAMETER StartStepId +The identification number of the first step to execute for the job. + +.PARAMETER Category +The category of the job. + +.PARAMETER OwnerLogin +The name of the login that owns the job. + +.PARAMETER EventLogLevel +Specifies when to place an entry in the Microsoft Windows application log for this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailLevel +Specifies when to send an e-mail upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER NetsendLevel +Specifies when to send a network message upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER PageLevel +Specifies when to send a page upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailOperator +The e-mail name of the operator to whom the e-mail is sent when EmailLevel is reached. + +.PARAMETER NetsendOperator +The name of the operator to whom the network message is sent. + +.PARAMETER PageOperator +The name of the operator to whom a page is sent. + +.PARAMETER DeleteLevel +Specifies when to delete the job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJob + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Description 'Just another job' +Creates a job with the name "Job1" and a small description + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Disabled +Creates the job but sets it to disabled + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -EventLogLevel OnSuccess +Creates the job and sets the notification to write to the Windows Application event log on success + +.EXAMPLE +New-DbaAgentJob -SqlInstance SSTAD-PC -Job 'Job One' -EmailLevel OnFailure -EmailOperator dba +Creates the job and sets the notification to send an e-mail to the e-mail operator + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1 -Job 'Job One' -Description 'Just another job' -Whatif +Doesn't create the job but shows what would happen. + +.EXAMPLE +New-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job 'Job One' +Creates a job with the name "Job One" on multiple servers + +.EXAMPLE +"sql1", "sql2", "sql3" | New-DbaAgentJob -Job 'Job One' +Creates a job with the name "Job One" on multiple servers using the pipe line +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Job, + [object[]]$Schedule, + [int[]]$ScheduleId, + [switch]$Disabled, + [string]$Description, + [int]$StartStepId, + [string]$Category, + [string]$OwnerLogin, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EventLogLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EmailLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [Parameter()] + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$PageLevel, + [string]$EmailOperator, + [string]$NetsendOperator, + [string]$PageOperator, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$DeleteLevel, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + # Check of the event log level is of type string and set the integer value + if ($EventLogLevel -notin 1, 2, 3) { + $EventLogLevel = switch ($EventLogLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the email level is of type string and set the integer value + if ($EmailLevel -notin 1, 2, 3) { + $EmailLevel = switch ($EmailLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the net send level is of type string and set the integer value + if ($NetsendLevel -notin 1, 2, 3) { + $NetsendLevel = switch ($NetsendLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the page level is of type string and set the integer value + if ($PageLevel -notin 1, 2, 3) { + $PageLevel = switch ($PageLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check of the delete level is of type string and set the integer value + if ($DeleteLevel -notin 1, 2, 3) { + $DeleteLevel = switch ($DeleteLevel) { + "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } + default { 0 } + } + } + + # Check the e-mail operator name + if (($EmailLevel -ge 1) -and (-not $EmailOperator)) { + Stop-Function -Message "Please set the e-mail operator when the e-mail level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($NetsendLevel -ge 1) -and (-not $NetsendOperator)) { + Stop-Function -Message "Please set the netsend operator when the netsend level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($PageLevel -ge 1) -and (-not $PageOperator)) { + Stop-Function -Message "Please set the page operator when the page level parameter is set." -Target $sqlinstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Check if the job already exists + if (-not $Force -and ($server.JobServer.Jobs.Name -contains $Job)) { + Stop-Function -Message "Job $Job already exists on $instance" -Target $instance -Continue + } + elseif ($Force -and ($server.JobServer.Jobs.Name -contains $Job)) { + Write-Message -Message "Job $Job already exists on $instance. Removing.." -Level Verbose + + if ($PSCmdlet.ShouldProcess($instance, "Removing the job $Job on $instance")) { + try { + Remove-DbaAgentJob -SqlInstance $instance -Job $Job -EnableException + } + catch { + Stop-Function -Message "Couldn't remove job $Job from $instance" -Target $instance -Continue -ErrorRecord $_ + } + } + + } + + if ($PSCmdlet.ShouldProcess($instance, "Creating the job on $instance")) { + # Create the job object + try { + $currentjob = New-Object Microsoft.SqlServer.Management.Smo.Agent.Job($server.JobServer, $Job) + } + catch { + Stop-Function -Message "Something went wrong creating the job. `n" -Target $Job -Continue -ErrorRecord $_ + } + + #region job options + # Settings the options for the job + if ($Disabled) { + Write-Message -Message "Setting job to disabled" -Level Verbose + $currentjob.IsEnabled = $false + } + else { + Write-Message -Message "Setting job to enabled" -Level Verbose + $currentjob.IsEnabled = $true + } + + if ($Description.Length -ge 1) { + Write-Message -Message "Setting job description" -Level Verbose + $currentjob.Description = $Description + } + + if ($StartStepId -ge 1) { + Write-Message -Message "Setting job start step id" -Level Verbose + $currentjob.StartStepID = $StartStepId + } + + if ($Category.Length -ge 1) { + # Check if the job category exists + if ($Category -notin $server.JobServer.JobCategories.Name) { + if ($Force) { + if ($PSCmdlet.ShouldProcess($instance, "Creating job category on $instance")) { + try { + # Create the category + New-DbaAgentJobCategory -SqlInstance $instance -Category $Category + } + catch { + Stop-Function -Message "Couldn't create job category $Category from $instance" -Target $instance -Continue -ErrorRecord $_ + } + } + } + else { + Stop-Function -Message "Job category $Category doesn't exist on $instance. Use -Force to create it." -Target $instance + return + } + } + else { + Write-Message -Message "Setting job category" -Level Verbose + $currentjob.Category = $Category + } + } + + if ($OwnerLogin.Length -ge 1) { + # Check if the login name is present on the instance + if ($server.Logins.Name -contains $OwnerLogin) { + Write-Message -Message "Setting job owner login name to $OwnerLogin" -Level Verbose + $currentjob.OwnerLoginName = $OwnerLogin + } + else { + Stop-Function -Message "The owner $OwnerLogin does not exist on instance $instance" -Target $Job -Continue + } + } + + if ($EventLogLevel -ge 0) { + Write-Message -Message "Setting job event log level" -Level Verbose + $currentjob.EventLogLevel = $EventLogLevel + } + + if ($EmailOperator) { + if ($EmailLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $EmailOperator) { + Write-Message -Message "Setting job e-mail level" -Level Verbose + $currentjob.EmailLevel = $EmailLevel + + Write-Message -Message "Setting job e-mail operator" -Level Verbose + $currentjob.OperatorToEmail = $EmailOperator + } + else { + Stop-Function -Message "The e-mail operator name $EmailOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Stop-Function -Message "Invalid combination of e-mail operator name $EmailOperator and email level $EmailLevel. Not setting the notification." -Target $Job -Continue + } + } + + if ($NetsendOperator) { + if ($NetsendLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $NetsendOperator) { + Write-Message -Message "Setting job netsend level" -Level Verbose + $currentjob.NetSendLevel = $NetsendLevel + + Write-Message -Message "Setting job netsend operator" -Level Verbose + $currentjob.OperatorToNetSend = $NetsendOperator + } + else { + Stop-Function -Message "The netsend operator name $NetsendOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Write-Message -Message "Invalid combination of netsend operator name $NetsendOperator and netsend level $NetsendLevel. Not setting the notification." + } + } + + if ($PageOperator) { + if ($PageLevel -ge 1) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $PageOperator) { + Write-Message -Message "Setting job pager level" -Level Verbose + $currentjob.PageLevel = $PageLevel + + Write-Message -Message "Setting job pager operator" -Level Verbose + $currentjob.OperatorToPage = $PageOperator + } + else { + Stop-Function -Message "The page operator name $PageOperator does not exist on instance $instance. Exiting.." -Target $Job -Continue + } + } + else { + Write-Message -Message "Invalid combination of page operator name $PageOperator and page level $PageLevel. Not setting the notification." -Level Warning + } + } + + if ($DeleteLevel -ge 0) { + Write-Message -Message "Setting job delete level" -Level Verbose + $currentjob.DeleteLevel = $DeleteLevel + } + #endregion job options + + try { + Write-Message -Message "Creating the job" -Level Verbose + + # Create the job + $currentjob.Create() + + Write-Message -Message "Job created with UID $($currentjob.JobID)" -Level Verbose + + # Make sure the target is set for the job + Write-Message -Message "Applying the target (local) to job $Job" -Level Verbose + $currentjob.ApplyToTargetServer("(local)") + + # If a schedule needs to be attached + if ($Schedule) { + Set-DbaAgentJob -SqlInstance $instance -Job $currentjob -Schedule $Schedule -SqlCredential $SqlCredential + } + + if ($ScheduleId) { + Set-DbaAgentJob -SqlInstance $instance -Job $currentjob -ScheduleId $ScheduleId -SqlCredential $SqlCredential + } + } + catch { + Stop-Function -Message "Something went wrong creating the job" -Target $currentjob -ErrorRecord $_ -Continue + } + } + + # Return the job + return $currentjob + } + } + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job(s)." -Level Verbose + } + +} diff --git a/functions/New-DbaAgentJobCategory.ps1 b/functions/New-DbaAgentJobCategory.ps1 new file mode 100644 index 000000000000..bee8a1a8e107 --- /dev/null +++ b/functions/New-DbaAgentJobCategory.ps1 @@ -0,0 +1,133 @@ +function New-DbaAgentJobCategory { + <# +.SYNOPSIS +New-DbaAgentJobCategory creates a new job category. + +.DESCRIPTION +New-DbaAgentJobCategory makes it possible to create a job category that can be used with jobs. +It returns an array of the job(s) created . + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER CategoryType +The type of category. This can be "LocalJob", "MultiServerJob" or "None". +The default is "LocalJob" and will automatically be set when no option is chosen. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJobCategory + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' + +Creates a new job category with the name 'Category 1'. + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 2' -CategoryType MultiServerJob + +Creates a new job category with the name 'Category 2' and assign the category type for a multi server job. + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [ValidateSet("LocalJob", "MultiServerJob", "None")] + [string]$CategoryType, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Check the category type + if (-not $CategoryType) { + # Setting category type to default + Write-Message -Message "Setting the category type to 'LocalJob'" -Level Verbose + $CategoryType = "LocalJob" + } + } + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($cat in $Category) { + # Check if the category already exists + if ($cat -in $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat already exists on $instance" -Target $instance -Continue + } + else { + if ($PSCmdlet.ShouldProcess($instance, "Adding the job category $cat")) { + try { + $jobcategory = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobCategory($server.JobServer, $cat) + $jobcategory.CategoryType = $CategoryType + + $jobcategory.Create() + + $server.JobServer.Refresh() + } + catch { + Stop-Function -Message "Something went wrong creating the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } # if should process + + } # end else category exists + + # Return the job category + Get-DbaAgentJobCategory -SqlInstance $instance -Category $cat + + } # for each category + + } # for each instance + } + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job category." -Level Verbose + } + +} \ No newline at end of file diff --git a/functions/New-DbaAgentJobStep.ps1 b/functions/New-DbaAgentJobStep.ps1 new file mode 100644 index 000000000000..a7b7a46c225e --- /dev/null +++ b/functions/New-DbaAgentJobStep.ps1 @@ -0,0 +1,393 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentJobStep { + <# +.SYNOPSIS +New-DbaAgentJobStep creates a new job step for a job + +.DESCRIPTION +New-DbaAgentJobStep creates a new job in the SQL Server Agent for a specific job + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job to which to add the step. + +.PARAMETER StepId +The sequence identification number for the job step. Step identification numbers start at 1 and increment without gaps. + +.PARAMETER StepName +The name of the step. + +.PARAMETER SubSystem +The subsystem used by the SQL Server Agent service to execute command. +Allowed values 'ActiveScripting','AnalysisCommand','AnalysisQuery','CmdExec','Distribution','LogReader','Merge','PowerShell','QueueReader','Snapshot','Ssis','TransactSql' +The default is 'TransactSql' + +.PARAMETER Command +The commands to be executed by SQLServerAgent service through subsystem. + +.PARAMETER CmdExecSuccessCode +The value returned by a CmdExec subsystem command to indicate that command executed successfully. + +.PARAMETER OnSuccessAction +The action to perform if the step succeeds. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnSuccessStepId +The ID of the step in this job to execute if the step succeeds and OnSuccessAction is "GoToStep". + +.PARAMETER OnFailAction +The action to perform if the step fails. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnFailStepId +The ID of the step in this job to execute if the step fails and OnFailAction is "GoToNextStep". + +.PARAMETER Database +The name of the database in which to execute a Transact-SQL step. The default is 'master'. + +.PARAMETER DatabaseUser +The name of the user account to use when executing a Transact-SQL step. + +.PARAMETER RetryAttempts +The number of retry attempts to use if this step fails. The default is 0. + +.PARAMETER RetryInterval +The amount of time in minutes between retry attempts. The default is 0. + +.PARAMETER OutputFileName +The name of the file in which the output of this step is saved. + +.PARAMETER Flag +Sets the flag(s) for the job step. + +Flag Description +---------------------------------------------------------------------------- +AppendAllCmdExecOutputToJobHistory Job history, including command output, is appended to the job history file. +AppendToJobHistory Job history is appended to the job history file. +AppendToLogFile Job history is appended to the SQL Server log file. +AppendToTableLog Job history is appended to a log table. +LogToTableWithOverwrite Job history is written to a log table, overwriting previous contents. +None Job history is not appended to a file. +ProvideStopProcessEvent Job processing is stopped. + +.PARAMETER ProxyName +The name of the proxy that the job step runs as. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Agent, Job, JobStep +Author: Sander Stad (@sqlstad, sqlstad.nl) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/New-DbaAgentJobStep + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 +Create a step in "Job1" with the name Step1 with the default subsystem TransactSql. + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the msdb + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers + +.EXAMPLE +New-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1, Job2, 'Job Three' -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers for multiple jobs + +.EXAMPLE +sql1, sql2, sql3 | New-DbaAgentJobStep -Job Job1 -StepName Step1 -Database msdb +Create a step in "Job1" with the name Step1 where the database will the "msdb" for multiple servers using pipeline +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $false)] + [int]$StepId, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [Parameter(Mandatory = $false)] + [ValidateSet('ActiveScripting', 'AnalysisCommand', 'AnalysisQuery', 'CmdExec', 'Distribution', 'LogReader', 'Merge', 'PowerShell', 'QueueReader', 'Snapshot', 'Ssis', 'TransactSql')] + [string]$Subsystem = 'TransactSql', + [Parameter(Mandatory = $false)] + [string]$Command, + [Parameter(Mandatory = $false)] + [int]$CmdExecSuccessCode, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnSuccessAction = 'QuitWithSuccess', + [Parameter(Mandatory = $false)] + [int]$OnSuccessStepId = 0, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnFailAction = 'QuitWithFailure', + [Parameter(Mandatory = $false)] + [int]$OnFailStepId, + [Parameter(Mandatory = $false)] + [object]$Database, + [Parameter(Mandatory = $false)] + [string]$DatabaseUser, + [Parameter(Mandatory = $false)] + [int]$RetryAttempts, + [Parameter(Mandatory = $false)] + [int]$RetryInterval, + [Parameter(Mandatory = $false)] + [string]$OutputFileName, + [Parameter(Mandatory = $false)] + [ValidateSet('AppendAllCmdExecOutputToJobHistory', 'AppendToJobHistory', 'AppendToLogFile', 'LogToTableWithOverwrite', 'None', 'ProvideStopProcessEvent')] + [string[]]$Flag, + [Parameter(Mandatory = $false)] + [string]$ProxyName, + [Parameter(Mandatory = $false)] + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Check the parameter on success step id + if (($OnSuccessAction -ne 'GoToStep') -and ($OnSuccessStepId -ge 1)) { + Stop-Function -Message "Parameter OnSuccessStepId can only be used with OnSuccessAction 'GoToStep'." -Target $SqlInstance + return + } + + # Check the parameter on success step id + if (($OnFailAction -ne 'GoToStep') -and ($OnFailStepId -ge 1)) { + Stop-Function -Message "Parameter OnFailStepId can only be used with OnFailAction 'GoToStep'." -Target $SqlInstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + Write-Message -Message "Job $j doesn't exists on $instance" -Warning + } + else { + # Create the job step object + try { + # Get the job + $currentjob = $Server.JobServer.Jobs[$j] + + # Create the job step + $JobStep = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobStep + + # Set the job where the job steps belongs to + $JobStep.Parent = $currentjob + } + catch { + Stop-Function -Message "Something went wrong creating the job step" -Target $instance -ErrorRecord $_ -Continue + } + + #region job step options + # Setting the options for the job step + if ($StepName) { + # Check if the step already exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + $JobStep.Name = $StepName + } + elseif (($Server.JobServer.Jobs[$j].JobSteps.Name -contains $StepName) -and $Force) { + Write-Message -Message "Step $StepName already exists for job. Force is used. Removing existing step" -Level Verbose + + # Remove the job step based on the name + Remove-DbaAgentJobStep -SqlInstance $instance -Job $currentjob -StepName $StepName + + # Set the name job step object + $JobStep.Name = $StepName + } + else { + Stop-Function -Message "The step name $StepName already exists for job $j" -Target $instance -Continue + } + } + elseif ($StepId) { + # Check if the used step id is already in place + if ($Job.JobSteps.ID -notcontains $StepId) { + Write-Message -Message "Setting job step step id to $StepId" -Level Verbose + $JobStep.ID = $StepId + } + elseif (($Job.JobSteps.ID -contains $StepId) -and $Force) { + Write-Message -Message "Step ID $StepId already exists for job. Force is used. Removing existing step" -Level Verbose + + # Remove the existing job step + $StepName = ($Server.JobServer.Jobs['Job2'].JobSteps | Where-Object {$_.ID -eq 1}).Name + Remove-DbaAgentJobStep -SqlInstance $instance -Job $currentjob -StepName $StepName + + # Set the ID job step object + $JobStep.ID = $StepId + } + else { + Stop-Function -Message "The step id $StepId already exists for job $j" -Target $instance -Continue + } + } + else { + # Get the job step count + $JobStep.ID = $Job.JobSteps.Count + 1 + } + + if ($Subsystem) { + Write-Message -Message "Setting job step subsystem to $Subsystem" -Level Verbose + $JobStep.Subsystem = $Subsystem + } + + if ($Command) { + Write-Message -Message "Setting job step command to $Command" -Level Verbose + $JobStep.Command = $Command + } + + if ($CmdExecSuccessCode) { + Write-Message -Message "Setting job step command exec success code to $CmdExecSuccessCode" -Level Verbose + $JobStep.CommandExecutionSuccessCode = $CmdExecSuccessCode + } + + if ($OnSuccessAction) { + Write-Message -Message "Setting job step success action to $OnSuccessAction" -Level Verbose + $JobStep.OnSuccessAction = $OnSuccessAction + } + + if ($OnSuccessStepId) { + Write-Message -Message "Setting job step success step id to $OnSuccessStepId" -Level Verbose + $JobStep.OnSuccessStep = $OnSuccessStepId + } + + if ($OnFailAction) { + Write-Message -Message "Setting job step fail action to $OnFailAction" -Level Verbose + $JobStep.OnFailAction = $OnFailAction + } + + if ($OnFailStepId) { + Write-Message -Message "Setting job step fail step id to $OnFailStepId" -Level Verbose + $JobStep.OnFailStep = $OnFailStepId + } + + if ($Database) { + # Check if the database is present on the server + if ($Server.Databases.Name -contains $Database) { + Write-Message -Message "Setting job step database name to $Database" -Level Verbose + $JobStep.DatabaseName = $Database + } + else { + Stop-Function -Message "The database is not present on instance $instance." -Target $instance -Continue + } + } + + if ($DatabaseUser -and $DatabaseName) { + # Check if the username is present in the database + if ($Server.Databases[$DatabaseName].Users.Name -contains $DatabaseUser) { + + Write-Message -Message "Setting job step database username to $DatabaseUser" -Level Verbose + $JobStep.DatabaseUserName = $DatabaseUser + } + else { + Stop-Function -Message "The database user is not present in the database $DatabaseName on instance $instance." -Target $instance -Continue + } + } + + if ($RetryAttempts) { + Write-Message -Message "Setting job step retry attempts to $RetryAttempts" -Level Verbose + $JobStep.RetryAttempts = $RetryAttempts + } + + if ($RetryInterval) { + Write-Message -Message "Setting job step retry interval to $RetryInterval" -Level Verbose + $JobStep.RetryInterval = $RetryInterval + } + + if ($OutputFileName) { + Write-Message -Message "Setting job step output file name to $OutputFileName" -Level Verbose + $JobStep.OutputFileName = $OutputFileName + } + + if ($ProxyName) { + # Check if the proxy exists + if ($Server.JobServer.ProxyAccounts.Name -contains $ProxyName) { + Write-Message -Message "Setting job step proxy name to $ProxyName" -Level Verbose + $JobStep.ProxyName = $ProxyName + } + else { + Stop-Function -Message "The proxy name $ProxyName doesn't exist on instance $instance." -Target $instance -Continue + } + } + + if ($Flag.Count -ge 1) { + Write-Message -Message "Setting job step flag(s) to $($Flags -join ',')" -Level Verbose + $JobStep.JobStepFlags = $Flag + } + #endregion job step options + + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Creating the job step $StepName")) { + try { + Write-Message -Message "Creating the job step" -Level Verbose + + # Create the job step + $JobStep.Create() + $currentjob.Alter() + } + catch { + Stop-Function -Message "Something went wrong creating the job step" -Target $instance -ErrorRecord $_ -Continue + } + } + + # Return the job step + $JobStep + } + } # foreach object job + } # foreach object instance + } # process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished creating job step(s)" -Level Verbose + } +} + diff --git a/functions/New-DbaAgentProxy.ps1 b/functions/New-DbaAgentProxy.ps1 new file mode 100644 index 000000000000..f3fe0c0fc5fd --- /dev/null +++ b/functions/New-DbaAgentProxy.ps1 @@ -0,0 +1,229 @@ +function New-DbaAgentProxy { + <# + .SYNOPSIS + Adds one or more proxies to SQL Server Agent + + .DESCRIPTION + Adds one or more proxies to SQL Server Agent + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The name of the proxy or proxies you want to create + + .PARAMETER Credential + The associated SQL Server Credential. The credential must be created prior to creating the Proxy. + + .PARAMETER SubSystem + The associated subsystem or subsystems. Defaults to CmdExec. + + Valid options include: + ActiveScripting + AnalysisCommand + AnalysisQuery + CmdExec + Distribution + LogReader + Merge + PowerShell + QueueReader + Snapshot + Ssis + TransactSql + + .PARAMETER Description + A description of the proxy + + .PARAMETER Login + The SQL Server login or logins (known as proxy principals) to assign to the proxy + + .PARAMETER ServerRole + The SQL Server role or roles (known as proxy principals) to assign to the proxy + + .PARAMETER MsdbRole + The msdb role or roles (known as proxy principals) to assign to the proxy + + .PARAMETER Disabled + Create the proxy as disabled + + .PARAMETER Force + Drop and recreate the proxy if it already exists + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Proxy + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaAgentProxy + + .EXAMPLE + New-DbaAgentProxy -SqlInstance sql2016 -Name STIG -Credential 'PowerShell Proxy' + + Creates an Agent Proxy on sql2016 with the name STIG with the 'PowerShell Proxy' credential. + The proxy is automatically added to the CmdExec subsystem. + + .EXAMPLE + New-DbaAgentProxy -SqlInstance localhost\sql2016 -Name STIG -Credential 'PowerShell Proxy' -Description "Used for auditing purposes" -Login ad\sqlstig -SubSystem CmdExec, PowerShell -ServerRole securtyadmin -MsdbRole ServerGroupAdministratorRole + + Creates an Agent Proxy on sql2016 with the name STIG with the 'PowerShell Proxy' credential and the following principals: + + Login: ad\sqlstig + ServerRole: securtyadmin + MsdbRole: ServerGroupAdministratorRole + + By default, only sysadmins have access to create job steps with proxies. This will allow 3 additional principals access: + The proxy is then added to the CmdExec and PowerShell subsystems + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] + param ( + [parameter(Mandatory, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string[]]$Name, + [parameter(Mandatory)] + [string[]]$Credential, + [ValidateSet("ActiveScripting", "AnalysisCommand", "AnalysisQuery", "CmdExec", "Distribution", "LogReader", "Merge", "PowerShell", "QueueReader", "Snapshot", "Ssis", "TransactSql")] + [string[]]$SubSystem = "CmdExec", + [string]$Description, + [string[]]$Login, + [string[]]$ServerRole, + [string[]]$MsdbRole, + [switch]$Disabled, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $jobServer = $server.JobServer + } + catch { + Stop-Function -Message "Failure. Is SQL Agent started?" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($proxyname in $name) { + + if ($jobServer.ProxyAccounts[$proxyName]) { + if ($force) { + if ($Pscmdlet.ShouldProcess($instance, "Dropping $proxyname")) { + $jobServer.ProxyAccounts[$proxyName].Drop() + $jobServer.ProxyAccounts.Refresh() + } + } + else { + Write-Message -Level Warning -Message "Proxy account $proxy already exists on $instance. Use -Force to drop and recreate." + continue + } + } + + if (-not $server.Credentials[$Credential]) { + Write-Message -Level Warning -Message "Credential '$Credential' does not exist on $instance" + continue + } + + if ($Pscmdlet.ShouldProcess($instance, "Adding $proxyname with the $Credential credential")) { + # the new-object is stubborn and $true/$false has to be forced in + $enabled = switch ($disabled) { + $false { + $proxy = New-Object Microsoft.SqlServer.Management.Smo.Agent.ProxyAccount -ArgumentList $jobServer, $ProxyName, $Credential, $true, $Description + } + $true { + $proxy = New-Object Microsoft.SqlServer.Management.Smo.Agent.ProxyAccount -ArgumentList $jobServer, $ProxyName, $Credential, $false, $Description + } + } + + try { + $proxy.Create() + } + catch { + Stop-Function -Message "Could not create proxy account" -ErrorRecord $_ -Target $instance -Continue + } + } + + foreach ($loginname in $login) { + if ($server.Logins[$loginname]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding login $loginname to proxy")) { + $proxy.AddLogin($loginname) + } + } + else { + Write-Message -Level Warning -Message "Login '$loginname' does not exist on $instance" + } + } + + foreach ($role in $ServerRole) { + if ($server.Roles[$role]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding server role $role to proxy")) { + $proxy.AddServerRole($role) + } + } + else { + Write-Message -Level Warning -Message "Server Role '$role' does not exist on $instance" + } + } + + foreach ($role in $MsdbRole) { + if ($server.Databases['msdb'].Roles[$role]) { + if ($Pscmdlet.ShouldProcess($instance, "Adding msdb role $role to proxy")) { + $proxy.AddMsdbRole($role) + } + } + else { + Write-Message -Level Warning -Message "msdb role '$role' does not exist on $instance" + } + } + + foreach ($system in $SubSystem) { + if ($Pscmdlet.ShouldProcess($instance, "Adding subsystem $system to proxy")) { + $proxy.AddSubSystem($system) + } + } + + if ($Pscmdlet.ShouldProcess("console", "Outputting Proxy object")) { + $proxy.Alter() + $proxy.Refresh() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name Logins -value $proxy.EnumLogins() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name ServerRoles -value $proxy.EnumServerRoles() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name MsdbRoles -value $proxy.EnumMsdbRoles() + Add-Member -Force -InputObject $proxy -MemberType NoteProperty -Name Subsystems -value $proxy.EnumSubSystems() + + Select-DefaultView -InputObject $proxy -Property ComputerName, InstanceName, SqlInstance, ID, Name, CredentialName, CredentialIdentity, Description, Logins, ServerRoles, MsdbRoles, SubSystems, IsEnabled + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaAgentSchedule.ps1 b/functions/New-DbaAgentSchedule.ps1 new file mode 100644 index 000000000000..03925999930d --- /dev/null +++ b/functions/New-DbaAgentSchedule.ps1 @@ -0,0 +1,642 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaAgentSchedule { + <# + .SYNOPSIS + New-DbaAgentSchedule creates a new schedule in the msdb database. + + .DESCRIPTION + New-DbaAgentSchedule will help create a new schedule for a job. + If the job parameter is not supplied the schedule will not be attached to a job. + + .PARAMETER SqlInstance + SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job that has the schedule. + + .PARAMETER Schedule + The name of the schedule. + + .PARAMETER Disabled + Set the schedule to disabled. Default is enabled + + .PARAMETER FrequencyType + A value indicating when a job is to be executed. + + Allowed values: Once, Daily, Weekly, Monthly, MonthlyRelative, AgentStart or IdleComputer + + If force is used the default will be "Once". + + .PARAMETER FrequencyInterval + The days that a job is executed + + Allowed values: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Weekdays, Weekend or EveryDay. + The other allowed values are the numbers 1 to 31 for each day of the month. + + If "Weekdays", "Weekend" or "EveryDay" is used it over writes any other value that has been passed before. + + If force is used the default will be 1. + + .PARAMETER FrequencySubdayType + Specifies the units for the subday FrequencyInterval. + + Allowed values: Time, Seconds, Minutes, or Hours + + .PARAMETER FrequencySubdayInterval + The number of subday type periods to occur between each execution of a job. + + .PARAMETER FrequencyRelativeInterval + A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + + Allowed values: First, Second, Third, Fourth or Last + + .PARAMETER FrequencyRecurrenceFactor + The number of weeks or months between the scheduled execution of a job. + + FrequencyRecurrenceFactor is used only if FrequencyType is "Weekly", "Monthly" or "MonthlyRelative". + + .PARAMETER StartDate + The date on which execution of a job can begin. + + If force is used the start date will be the current day + + .PARAMETER EndDate + The date on which execution of a job can stop. + + If force is used the end date will be '9999-12-31' + + .PARAMETER StartTime + The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + If force is used the start time will be '00:00:00' + + .PARAMETER EndTime + The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. + Example: '010000' for 01:00:00 AM. + Example: '140000' for 02:00:00 PM. + + If force is used the start time will be '23:59:59' + + .PARAMETER Owner + The name of the server principal that owns the schedule. If no value is given the schedule is owned by the creator. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + The force parameter will ignore some errors in the parameters and assume defaults. + It will also remove the any present schedules with the same name for the specific job. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, JobStep + Author: Sander Stad (@sqlstad, sqlstad.nl) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaAgentSchedule + + .EXAMPLE + New-DbaAgentSchedule -SqlInstance localhost\SQL2016 -Schedule daily -FrequencyType Daily -FrequencyInterval Everyday -Force + + Creates a schedule with a daily frequency every day. It assumes default values for the start date, start time, end date and end time due to -Force. + + .EXAMPLE + New-DbaAgentSchedule -SqlInstance sstad-pc -Schedule MonthlyTest -FrequencyType Monthly -FrequencyInterval 10 -FrequencyRecurrenceFactor 1 -Force + + Create a schedule with a monhtly frequency occuring every 10th of the month. It assumes default values for the start date, start time, end date and end time due to -Force. + + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential] + $SqlCredential, + [object[]]$Job, + [object]$Schedule, + [switch]$Disabled, + [ValidateSet('Once', 'Daily', 'Weekly', 'Monthly', 'MonthlyRelative', 'AgentStart', 'IdleComputer')] + [object]$FrequencyType, + [ValidateSet('EveryDay', 'Weekdays', 'Weekend', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31)] + [object[]]$FrequencyInterval, + [ValidateSet('Time', 'Seconds', 'Minutes', 'Hours')] + [object]$FrequencySubdayType, + [int]$FrequencySubdayInterval, + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$FrequencyRelativeInterval, + [int]$FrequencyRecurrenceFactor, + [string]$StartDate, + [string]$EndDate, + [string]$StartTime, + [string]$EndTime, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # if a Schedule is not provided there is no much point + if (!$Schedule) { + Stop-Function -Message "A schedule was not provided! Please provide a schedule name." + return + } + + [int]$Interval = 0 + + # Translate FrequencyType value from string to the integer value + if (!$FrequencyType -or $FrequencyType) { + [int]$FrequencyType = + switch ($FrequencyType) { + "Once" { 1 } + "Daily" { 4 } + "Weekly" { 8 } + "Monthly" { 16 } + "MonthlyRelative" { 32 } + "AgentStart" { 64 } + "IdleComputer" { 128 } + default { 1 } + } + } + + # Translate FrequencySubdayType value from string to the integer value + if (!$FrequencySubdayType -or $FrequencySubdayType) { + [int]$FrequencySubdayType = + switch ($FrequencySubdayType) { + "Time" { 1 } + "Seconds" { 2 } + "Minutes" { 4 } + "Hours" { 8 } + default { 1 } + } + } + + # Check of the relative FrequencyInterval value is of type string and set the integer value + [int]$FrequencyRelativeInterval = + switch ($FrequencyRelativeInterval) { + "First" { 1 } + "Second" { 2 } + "Third" { 4 } + "Fourth" { 8 } + "Last" { 16 } + "Unused" { 0 } + default {0} + } + + # Check if the interval is valid + if (($FrequencyType -in 4, "Daily") -and (($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365) -and -not $FrequencyInterval -eq "EveryDay")) { + Stop-Function -Message "The frequency interval $FrequencyInterval requires a frequency interval to be between 1 and 365." -Target $SqlInstance + return + } + + # Check if the recurrence factor is set for weekly or monthly interval + if (($FrequencyType -in (16, 8)) -and $FrequencyRecurrenceFactor -lt 1) { + if ($Force) { + $FrequencyRecurrenceFactor = 1 + Write-Message -Message "Recurrence factor not set for weekly or monthly interval. Setting it to $FrequencyRecurrenceFactor." -Level Verbose + } + else { + Stop-Function -Message "The recurrence factor $FrequencyRecurrenceFactor (parameter FrequencyRecurrenceFactor) needs to be at least one when using a weekly or monthly interval." -Target $SqlInstance + return + } + } + + # Check the subday interval + if (($FrequencySubdayType -in 2, "Seconds", 4, "Minutes") -and (-not ($FrequencySubdayInterval -ge 1 -or $FrequencySubdayInterval -le 59))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 59 when subday type is 'Seconds' or 'Minutes'" -Target $SqlInstance + return + } + elseif (($FrequencySubdayType -eq 8, "Hours") -and (-not ($FrequencySubdayInterval -ge 1 -and $FrequencySubdayInterval -le 23))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 23 when subday type is 'Hours'" -Target $SqlInstance + return + } + + # If the FrequencyInterval is set for the daily FrequencyType + if ($FrequencyType -in 4, 'Daily') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Create the interval to hold the value(s) + switch ($FrequencyInterval) { + "EveryDay" { $Interval = 1} + default {$Interval = 1 } + } + + } + + # If the FrequencyInterval is set for the weekly FrequencyType + if ($FrequencyType -in 8, 'Weekly') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 4 } + "Wednesday" { $Interval += 8 } + "Thursday" { $Interval += 16 } + "Friday" { $Interval += 32 } + "Saturday" { $Interval += 64 } + "Weekdays" { $Interval = 62 } + "Weekend" { $Interval = 65 } + "EveryDay" {$Interval = 127 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 4 { $Interval += 4 } + 8 { $Interval += 8 } + 16 { $Interval += 16 } + 32 { $Interval += 32 } + 64 { $Interval += 64 } + 62 { $Interval = 62 } + 65 { $Interval = 65 } + 127 {$Interval = 127 } + default { $Interval = 0 } + } + } + } + + # If the FrequencyInterval is set for the monthly FrequencyInterval + if ($FrequencyType -in 16, 'Monthly') { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + $FrequencyInterval + switch ($Item) { + {[int]$_ -ge 1 -and [int]$_ -le 31} { $Interval = [int]$Item } + } + } + + + } + + # If the FrequencyInterval is set for the relative monthly FrequencyInterval + if ($FrequencyType -eq 32) { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 3 } + "Wednesday" { $Interval += 4 } + "Thursday" { $Interval += 5 } + "Friday" { $Interval += 6 } + "Saturday" { $Interval += 7 } + "Day" { $Interval += 8 } + "Weekday" { $Interval += 9 } + "WeekendDay" { $Interval += 10 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 3 { $Interval += 3 } + 4 { $Interval += 4 } + 5 { $Interval += 5 } + 6 { $Interval += 6 } + 7 { $Interval += 7 } + 8 { $Interval += 8 } + 9 { $Interval += 9 } + 10 { $Interval += 10 } + } + } + } + + # Check if the interval is valid for the frequency + if ($FrequencyType -eq 0) { + if ($Force) { + Write-Message -Message "Parameter FrequencyType must be set to at least [Once]. Setting it to 'Once'." -Level Warning + $FrequencyType = 1 + } + else { + Stop-Function -Message "Parameter FrequencyType must be set to at least [Once]" -Target $SqlInstance + return + } + } + + # Check if the interval is valid for the frequency + if (($FrequencyType -in 4, 8, 32) -and ($Interval -lt 1)) { + if ($Force) { + Write-Message -Message "Parameter FrequencyInterval must be provided for a recurring schedule. Setting it to first day of the week." -Level Warning + $Interval = 1 + } + else { + Stop-Function -Message "Parameter FrequencyInterval must be provided for a recurring schedule." -Target $SqlInstance + return + } + } + + # Setup the regex + $RegexDate = '(? + [CmdletBinding()] + Param ( + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(Mandatory, ValueFromPipeline)] + [DbaInstanceParameter[]]$ServerName, + [parameter(Mandatory)] + [string]$Alias, + [ValidateSet("TCPIP", "NamedPipes")] + [string]$Protocol = "TCPIP", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # This is a script block so cannot use messaging system + $scriptblock = { + $basekeys = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer", "HKLM:\SOFTWARE\Microsoft\MSSQLServer" + $ServerName = $args[0] + $Alias = $args[1] + $serverstring = $args[2] + + if ($env:PROCESSOR_ARCHITECTURE -like "*64*") { $64bit = $true } + + foreach ($basekey in $basekeys) { + if ($64bit -ne $true -and $basekey -like "*WOW64*") { continue } + + if ((Test-Path $basekey) -eq $false) { + throw "Base key ($basekey) does not exist. Quitting." + } + + $client = "$basekey\Client" + + if ((Test-Path $client) -eq $false) { + # "Creating $client key" + $null = New-Item -Path $client -Force + } + + $connect = "$client\ConnectTo" + + if ((Test-Path $connect) -eq $false) { + # "Creating $connect key" + $null = New-Item -Path $connect -Force + } + + if ($basekey -like "*WOW64*") { + $architecture = "32-bit" + } + else { + $architecture = "64-bit" + } + + # Write-Verbose "Creating/updating alias for $ComputerName for $architecture" + $null = New-ItemProperty -Path $connect -Name $Alias -Value $serverstring -PropertyType String -Force + } + } + } + + process { + if ($protocol -eq "TCPIP") { + $serverstring = "DBMSSOCN,$ServerName" + } + else { + $serverstring = "DBNMPNTW,\\$ServerName\pipe\sql\query" + } + + foreach ($computer in $ComputerName.ComputerName) { + + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + if ($PScmdlet.ShouldProcess($computer, "Adding $alias")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ErrorAction Stop -ArgumentList $ServerName, $Alias, $serverstring + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + + Get-DbaClientAlias -ComputerName $computer -Credential $Credential | Where-Object AliasName -eq $Alias + } +} \ No newline at end of file diff --git a/functions/New-DbaCmConnection.ps1 b/functions/New-DbaCmConnection.ps1 new file mode 100644 index 000000000000..2a005129d22e --- /dev/null +++ b/functions/New-DbaCmConnection.ps1 @@ -0,0 +1,172 @@ +function New-DbaCmConnection { + <# + .SYNOPSIS + Generates a connection object for use in remote computer management. + + .DESCRIPTION + Generates a connection object for use in remote computer management. + Those objects are used for the purpose of cim/wmi queries, caching which protocol worked, optimizing performance and minimizing authentication errors. + + New-DbaCmConnection will create a NEW object and overwrite any existing ones for the specified computer. + Furthermore, information stored in the input beyond the computername will be discarded in favor of the new settings. + + Unless the connection cache has been disabled, all connections will automatically be registered in the cache, so no further action is necessary. + The output is primarily for information purposes, however it may be used to pass objects and circumvent the cache with those. + + NOTE: Generally, this function need not be used, as a first connection to a computer using any connecting function such as "Get-DbaCmObject" will automatically register a new default connection for it. + + This function exists to be able to preconfigure connections. + + .PARAMETER ComputerName + The computer to build the connection object for. + + .PARAMETER Credential + The credential to register. + + .PARAMETER UseWindowsCredentials + Whether using the default windows credentials is legit. + Not setting this will not exclude using windows credentials, but only not pre-confirm them as working. + + .PARAMETER OverrideExplicitCredential + Setting this will enable the credential override. + The override will cause the system to ignore explicitly specified credentials, so long as known, good credentials are available. + + .PARAMETER DisabledConnectionTypes + Exlicitly disable connection types. + These types will then not be used for connecting to the computer. + + .PARAMETER DisableBadCredentialCache + Will prevent the caching of credentials if set to true. + + .PARAMETER DisableCimPersistence + Will prevent Cim-Sessions to be reused. + + .PARAMETER DisableCredentialAutoRegister + Will prevent working credentials from being automatically cached + + .PARAMETER EnableCredentialFailover + Will enable automatic failing over to known to work credentials, when using bad credentials. + By default, passing bad credentials will cause the Computer Management functions to interrupt with a warning (Or exception if in silent mode). + + .PARAMETER WindowsCredentialsAreBad + Will prevent the windows credentials of the currently logged on user from being used for the remote connection. + + .PARAMETER CimWinRMOptions + Specify a set of options to use when connecting to the target computer using CIM over WinRM. + Use 'New-CimSessionOption' to create such an object. + + .PARAMETER CimDCOMOptions + Specify a set of options to use when connecting to the target computer using CIM over DCOM. + Use 'New-CimSessionOption' to create such an object. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaCmConnection + + .EXAMPLE + New-DbaCmConnection -ComputerName sql2014 -UseWindowsCredentials -OverrideExplicitCredential -DisabledConnectionTypes CimRM + + Returns a new configuration object for connecting to the computer sql2014. + - The current user credentials are set as valid + - The connection is configured to ignore explicit credentials (so all connections use the windows credentials) + - The connections will not try using CIM over WinRM + + Unless caching is globally disabled, this is automatically stored in the connection cache and will be applied automatically. + In that (the default) case, the output is for information purposes only and need not be used. + + .EXAMPLE + Get-Content computers.txt | New-DbaCmConnection -Credential $cred -CimWinRMOptions $options -DisableBadCredentialCache -OverrideExplicitCredential + + Gathers a list of computers from a text file, then creates and registers connections for each of them, setting them to ... + - use the credentials stored in $cred + - use the options stored in $options when connecting using CIM over WinRM + - not store credentials that are known to not work + - to ignore explicitly specified credentials + + Essentially, this configures all connections to those computers to prefer failure with the specified credentials over using alternative credentials. + #> + [CmdletBinding(DefaultParameterSetName = 'Credential')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName = $env:COMPUTERNAME, + [Parameter(ParameterSetName = "Credential")] + [PSCredential] + $Credential, + [Parameter(ParameterSetName = "Windows")] + [switch] + $UseWindowsCredentials, + [switch] + $OverrideExplicitCredential, + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType] + $DisabledConnectionTypes = 'None', + [switch] + $DisableBadCredentialCache, + [switch] + $DisableCimPersistence, + [switch] + $DisableCredentialAutoRegister, + [switch] + $EnableCredentialFailover, + [Parameter(ParameterSetName = "Credential")] + [switch] + $WindowsCredentialsAreBad, + [Microsoft.Management.Infrastructure.Options.WSManSessionOptions] + $CimWinRMOptions, + [Microsoft.Management.Infrastructure.Options.DComSessionOptions] + $CimDCOMOptions, + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + Write-Message -Level InternalComment -Message "Starting execution" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $disable_cache = Get-DbaConfigValue -Name 'ComputerManagement.Cache.Disable.All' -Fallback $false + } + process { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Processing computer: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection + + $connection = New-Object -TypeName Sqlcollaborative.Dbatools.Connection.ManagementConnection -ArgumentList $connectionObject.Connection.ComputerName + if (Test-Bound "Credential") { $connection.Credentials = $Credential } + if (Test-Bound "UseWindowsCredentials") { + $connection.Credentials = $null + $connection.UseWindowsCredentials = $UseWindowsCredentials + } + if (Test-Bound "OverrideExplicitCredential") { $connection.OverrideExplicitCredential = $OverrideExplicitCredential } + if (Test-Bound "DisabledConnectionTypes") { $connection.DisabledConnectionTypes = $DisabledConnectionTypes } + if (Test-Bound "DisableBadCredentialCache") { $connection.DisableBadCredentialCache = $DisableBadCredentialCache } + if (Test-Bound "DisableCimPersistence") { $connection.DisableCimPersistence = $DisableCimPersistence } + if (Test-Bound "DisableCredentialAutoRegister") { $connection.DisableCredentialAutoRegister = $DisableCredentialAutoRegister } + if (Test-Bound "EnableCredentialFailover") { $connection.DisableCredentialAutoRegister = $EnableCredentialFailover } + if (Test-Bound "WindowsCredentialsAreBad") { $connection.WindowsCredentialsAreBad = $WindowsCredentialsAreBad } + if (Test-Bound "CimWinRMOptions") { $connection.CimWinRMOptions = $CimWinRMOptions } + if (Test-Bound "CimDCOMOptions") { $connection.CimDCOMOptions = $CimDCOMOptions } + + if (-not $disable_cache) { + Write-Message -Level Verbose -Message "Writing connection to cache" + [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$connectionObject.Connection.ComputerName] = $connection + } + else { Write-Message -Level Verbose -Message "Skipping writing to cache, since the cache has been disabled!" } + $connection + } + } + end { + Write-Message -Level InternalComment -Message "Stopping execution" + } +} diff --git a/functions/New-DbaComputerCertificate.ps1 b/functions/New-DbaComputerCertificate.ps1 new file mode 100644 index 000000000000..0a538b31be1d --- /dev/null +++ b/functions/New-DbaComputerCertificate.ps1 @@ -0,0 +1,394 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaComputerCertificate { + <# + .SYNOPSIS + Creates a new computer certificate useful for Forcing Encryption + + .DESCRIPTION + Creates a new computer certificate - self-signed or signed by an Active Directory CA, using the Web Server certificate. + + By default, a key with a length of 1024 and a friendly name of the machines FQDN is generated. + + This command was originally intended to help automate the process so that SSL certificates can be available for enforcing encryption on connections. + + It makes a lot of assumptions - namely, that your account is allowed to auto-enroll and that you have permission to do everything it needs to do ;) + + References: + http://sqlmag.com/sql-server/7-steps-ssl-encryption + https://azurebi.jppp.org/2016/01/23/using-lets-encrypt-certificates-for-secure-sql-server-connections/ + https://blogs.msdn.microsoft.com/sqlserverfaq/2016/09/26/creating-and-registering-ssl-certificates/ + + The certificate is generated using AD's webserver SSL template on the client machine and pushed to the remote machine. + + .PARAMETER ComputerName + The target SQL Server - defaults to localhost. If target is a cluster, you must also specify ClusterInstanceName (see below) + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. + + .PARAMETER CaServer + Optional - the CA Server where the request will be sent to + + .PARAMETER CaName + The properly formatted CA name of the corresponding CaServer + + .PARAMETER ClusterInstanceName + When creating certs for a cluster, use this parameter to create the certificate for the cluster node name. Use ComputerName for each of the nodes. + + .PARAMETER Password + Password to encrypt/decrypt private key for export to remote machine + + .PARAMETER FriendlyName + The FriendlyName listed in the certificate. This defaults to the FQDN of the $ComputerName + + .PARAMETER CertificateTemplate + The domain's Certificate Template - WebServer by default. + + .PARAMETER KeyLength + The length of the key - defaults to 1024 + + .PARAMETER Store + Certificate store - defaults to LocalMachine + + .PARAMETER Folder + Certificate folder - defaults to My (Personal) + + .PARAMETER Dns + Specify the Dns entries listed in SAN. By default, it will be ComputerName + FQDN, or in the case of clusters, clustername + cluster FQDN. + + .PARAMETER SelfSigned + Creates a self-signed certificate. All other parameters can still apply except CaServer and CaName because the command does not go and get the certificate signed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + New-DbaComputerCertificate + + Creates a computer certificate signed by the local domain CA for the local machine with the keylength of 1024. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName Server1 + + Creates a computer certificate signed by the local domain CA _on the local machine_ for server1 with the keylength of 1024. + + The certificate is then copied to the new machine over WinRM and imported. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName sqla, sqlb -ClusterInstanceName sqlcluster -KeyLength 4096 + + Creates a computer certificate for sqlcluster, signed by the local domain CA, with the keylength of 4096. + + The certificate is then copied to sqla _and_ sqlb over WinRM and imported. + + .EXAMPLE + New-DbaComputerCertificate -ComputerName Server1 -WhatIf + + Shows what would happen if the command were run + + .EXAMPLE + New-DbaComputerCertificate -SelfSigned + + Creates a self-signed certificate + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [string]$CaServer, + [string]$CaName, + [string]$ClusterInstanceName, + [securestring]$Password, + [string]$FriendlyName = "SQL Server", + [string]$CertificateTemplate = "WebServer", + [int]$KeyLength = 1024, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [string[]]$Dns, + [switch]$SelfSigned, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $englishCodes = 9, 1033, 2057, 3081, 4105, 5129, 6153, 7177, 8201, 9225 + if ($englishCodes -notcontains (Get-DbaCmObject Win32_OperatingSystem).OSLanguage) { + Stop-Function -Message "Currently, this command is only supported in English OS locales. OS Locale detected: $([System.Globalization.CultureInfo]::GetCultureInfo([int](Get-DbaCmObject Win32_OperatingSystem).OSLanguage).DisplayName)`nWe apologize for the inconvenience and look into providing universal language support in future releases." + return + } + + if (-not (Test-ElevationRequirement -ComputerName $env:COMPUTERNAME)) { + return + } + + function GetHexLength { + [cmdletbinding()] + param( + [int]$strLen + ) + $hex = [String]::Format("{0:X2}", $strLen) + + if ($strLen -gt 127) { [String]::Format("{0:X2}", 128 + ($hex.Length / 2)) + $hex } + else { $hex } + } + + function Get-SanExt { + [cmdletbinding()] + param( + [string[]]$hostName + ) + # thanks to Lincoln of + # https://social.technet.microsoft.com/Forums/windows/en-US/f568edfa-7f93-46a4-aab9-a06151592dd9/converting-ascii-to-asn1-der + + $temp = '' + foreach ($fqdn in $hostName) { + # convert each character of fqdn to hex + $hexString = ($fqdn.ToCharArray() | ForEach-Object { [String]::Format("{0:X2}", [int]$_) }) -join '' + + # length of hex fqdn, in hex + $hexLength = GetHexLength ($hexString.Length / 2) + + # concatenate special code 82, hex length, hex string + $temp += "82${hexLength}${hexString}" + } + # calculate total length of concatenated string, in hex + $totalHexLength = GetHexLength ($temp.Length / 2) + # concatenate special code 30, hex length, hex string + $temp = "30${totalHexLength}${temp}" + # convert to binary + $bytes = $( + for ($i = 0; $i -lt $temp.Length; $i += 2) { + [byte]"0x$($temp.SubString($i, 2))" + } + ) + # convert to base 64 + $base64 = [Convert]::ToBase64String($bytes) + # output in proper format + for ($i = 0; $i -lt $base64.Length; $i += 64) { + $line = $base64.SubString($i, [Math]::Min(64, $base64.Length - $i)) + if ($i -eq 0) { "2.5.29.17=$line" } + else { "_continue_=$line" } + } + } + + if ((!$CaServer -or !$CaName) -and !$SelfSigned) { + try { + Write-Message -Level Verbose -Message "No CaServer or CaName specified. Performing lookup." + # hat tip Vadims Podans + $domain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name + $domain = "DC=" + $domain -replace '\.', ", DC=" + $pks = [ADSI]"LDAP://CN=Enrollment Services, CN=Public Key Services, CN=Services, CN=Configuration, $domain" + $cas = $pks.psBase.Children + + $allCas = @() + foreach ($ca in $cas) { + $allCas += [pscustomobject]@{ + CA = $ca | ForEach-Object { $_.Name } + Computer = $ca | ForEach-Object { $_.DNSHostName } + } + } + } + catch { + Stop-Function -Message "Cannot access Active Directory or find the Certificate Authority" -ErrorRecord $_ + return + } + + if (!$CaServer) { + $CaServer = ($allCas | Select-Object -First 1).Computer + Write-Message -Level Verbose -Message "Root Server: $CaServer" + } + + if (!$CaName) { + $CaName = ($allCas | Select-Object -First 1).CA + Write-Message -Level Verbose -Message "Root CA name: $CaName" + } + } + + $tempDir = ([System.IO.Path]::GetTempPath()).TrimEnd("\") + $certTemplate = "CertificateTemplate:$CertificateTemplate" + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($computer in $ComputerName) { + + if (!$secondaryNode) { + + if ($ClusterInstanceName) { + if ($ClusterInstanceName -notmatch "\.") { + $fqdn = "$ClusterInstanceName.$env:USERDNSDOMAIN" + } + else { + $fqdn = $ClusterInstanceName + } + } + else { + $resolved = Resolve-DbaNetworkName -ComputerName $computer.ComputerName -WarningAction SilentlyContinue + + if (!$resolved) { + $fqdn = "$ComputerName.$env:USERDNSDOMAIN" + Write-Message -Level Warning -Message "Server name cannot be resolved. Guessing it's $fqdn" + } + else { + $fqdn = $resolved.fqdn + } + } + + $certDir = "$tempDir\$fqdn" + $certCfg = "$certDir\request.inf" + $certCsr = "$certDir\$fqdn.csr" + $certCrt = "$certDir\$fqdn.crt" + $certPfx = "$certDir\$fqdn.pfx" + $tempPfx = "$certDir\temp-$fqdn.pfx" + + if (Test-Path($certDir)) { + Write-Message -Level Output -Message "Deleting files from $certDir" + $null = Remove-Item "$certDir\*.*" + } + else { + Write-Message -Level Output -Message "Creating $certDir" + $null = New-Item -Path $certDir -ItemType Directory -Force + } + + # Make sure output is compat with clusters + $shortName = $fqdn.Split(".")[0] + + if (!$dns) { + $dns = $shortName, $fqdn + } + + $san = Get-SanExt $dns + # Write config file + Set-Content $certCfg "[Version]" + Add-Content $certCfg 'Signature="$Windows NT$"' + Add-Content $certCfg "[NewRequest]" + Add-Content $certCfg "Subject = ""CN=$fqdn""" + Add-Content $certCfg "KeySpec = 1" + Add-Content $certCfg "KeyLength = $KeyLength" + Add-Content $certCfg "Exportable = TRUE" + Add-Content $certCfg "MachineKeySet = TRUE" + Add-Content $certCfg "FriendlyName=""$FriendlyName""" + Add-Content $certCfg "SMIME = False" + Add-Content $certCfg "PrivateKeyArchive = FALSE" + Add-Content $certCfg "UserProtected = FALSE" + Add-Content $certCfg "UseExistingKeySet = FALSE" + Add-Content $certCfg "ProviderName = ""Microsoft RSA SChannel Cryptographic Provider""" + Add-Content $certCfg "ProviderType = 12" + if ($SelfSigned) { + Add-Content $certCfg "RequestType = Cert" + } + else { + Add-Content $certCfg "RequestType = PKCS10" + } + Add-Content $certCfg "KeyUsage = 0xa0" + Add-Content $certCfg "[EnhancedKeyUsageExtension]" + Add-Content $certCfg "OID=1.3.6.1.5.5.7.3.1" + Add-Content $certCfg "[Extensions]" + Add-Content $certCfg $san + Add-Content $certCfg "Critical=2.5.29.17" + + + if ($PScmdlet.ShouldProcess("local", "Creating certificate for $computer")) { + Write-Message -Level Output -Message "Running: certreq -new $certCfg $certCsr" + $create = certreq -new $certCfg $certCsr + } + + if ($SelfSigned) { + $serial = (($create -Split "Serial Number:" -Split "Subject")[2]).Trim() # D: + $storedCert = Get-ChildItem Cert:\LocalMachine\My -Recurse | Where-Object SerialNumber -eq $serial + + if ($computer.IsLocalHost) { + $storedCert | Select-Object * | Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + } + else { + if ($PScmdlet.ShouldProcess("local", "Submitting certificate request for $computer to $CaServer\$CaName")) { + Write-Message -Level Output -Message "certreq -submit -config `"$CaServer\$CaName`" -attrib $certTemplate $certCsr $certCrt $certPfx" + $submit = certreq -submit -config ""$CaServer\$CaName"" -attrib $certTemplate $certCsr $certCrt $certPfx + } + + if ($submit -match "ssued") { + Write-Message -Level Output -Message "certreq -accept -machine $certCrt" + $null = certreq -accept -machine $certCrt + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($certCrt, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) + $storedCert = Get-ChildItem "Cert:\$store\$folder" -Recurse | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + } + elseif ($submit) { + Write-Message -Level Warning -Message "Something went wrong" + Write-Message -Level Warning -Message "$create" + Write-Message -Level Warning -Message "$submit" + Stop-Function -Message "Failure when attempting to create the cert on $computer. Exception: $_" -ErrorRecord $_ -Target $computer -Continue + } + + if ($Computer.IsLocalHost) { + $storedCert | Select-Object * | Select-DefaultView -Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + } + } + + if (!$Computer.IsLocalHost) { + + if (!$secondaryNode) { + if ($PScmdlet.ShouldProcess("local", "Generating pfx and reading from disk")) { + Write-Message -Level Output -Message "Exporting PFX with password to $tempPfx" + $certdata = $storedCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::PFX, $password) + } + + if ($PScmdlet.ShouldProcess("local", "Removing cert from disk but keeping it in memory")) { + $storedCert | Remove-Item + } + + if ($ClusterInstanceName) { $secondaryNode = $true } + } + + $scriptblock = { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($args[0], $args[1], "Exportable,PersistKeySet") + + $certstore = New-Object System.Security.Cryptography.X509Certificates.X509Store($args[3], $args[2]) + $certstore.Open('ReadWrite') + $certstore.Add($cert) + $certstore.Close() + Get-ChildItem "Cert:\$($args[2])\$($args[3])" -Recurse | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $computer to import new cert")) { + try { + Write-Message -Level Output -Message "Connecting to $computer" + Invoke-Command2 -ComputerName $computer -Credential $Credential -ArgumentList $certdata, $Password, $Store, $Folder -ScriptBlock $scriptblock -ErrorAction Stop | + Select-DefaultView -Property DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer + } + catch { + Stop-Function -Message "Issue importing new cert on $computer" -ErrorRecord $_ -Target $computer -Continue + } + } + } + if ($PScmdlet.ShouldProcess("local", "Removing all files from $certDir")) { + try { + Remove-Item -Force -Recurse $certDir -ErrorAction SilentlyContinue + } + catch { + Stop-Function "Isue removing files from $certDir" -Target $certDir -ErrorRecord $_ + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaCredential.ps1 b/functions/New-DbaCredential.ps1 new file mode 100644 index 000000000000..c83e30a5af2b --- /dev/null +++ b/functions/New-DbaCredential.ps1 @@ -0,0 +1,150 @@ +function New-DbaCredential { + <# +.SYNOPSIS +Creates a new SQL Server credential + +.DESCRIPTION +Creates a new credential + +.PARAMETER SqlInstance +The target SQL Server(s) + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials + +.PARAMETER Name +The Credential name + +.PARAMETER Identity +The Credential Identity + +.PARAMETER Password +Secure string used to authenticate the Credential Identity + +.PARAMETER MappedClassType +Sets the class associated with the credential. + +.PARAMETER ProviderName +Sets the name of the provider + +.PARAMETER Force +If credential exists, drop and recreate + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 + +You will be prompted to securely enter your password, then a credential will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a credential in the 'db1' database + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Name AzureBackupBlobStore -Identity '' -Password (ConvertTo-SecureString '' -AsPlainText -Force) + +Create credential on SQL Server 2012 CU2, SQL Server 2014 for use with BACKUP TO URL. +CredentialIdentity needs to be supplied with the Azure Storage Account Name. +Password needs to be one of the Access Keys for the account. + +.EXAMPLE +New-DbaCredential -SqlInstance Server1 -Name 'https://.blob.core.windows.net/' -Identity 'SHARED ACCESS SIGNATURE' -Password (ConvertTo-SecureString '' -AsPlainText -Force) + +Create Credential on SQL Server 2016 or higher for use with BACKUP TO URL. +Name has to be the full URL for the blob store container that will be the backup target. +Password needs to be passed the Shared Access Token (SAS Key). + +#> + [CmdletBinding(SupportsShouldProcess = $true)] #, ConfirmImpact = "High" + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Name = $Identity, + [parameter(Mandatory)] + [Alias("CredentialIdentity")] + [string[]]$Identity, + [Security.SecureString]$Password, + [ValidateSet('CryptographicProvider', 'None')] + [string]$MappedClassType = "None", + [string]$ProviderName, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $mappedclass = switch ($MappedClassType) { + "CryptographicProvider" { 1 } + "None" { 0 } + } + } + + process { + if (!$Password) { + Read-Host -AsSecureString -Prompt "Enter the credential password" + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($cred in $Identity) { + $currentcred = $server.Credentials[$name] + + if ($currentcred) { + if ($force) { + Write-Message -Level Verbose -Message "Dropping credential $name" + $currentcred.Drop() + } + else { + Stop-Function -Message "Credential exists and Force was not specified" -Target $name -Continue + } + } + + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating credential for database '$cred' on $instance")) { + try { + $credential = New-Object Microsoft.SqlServer.Management.Smo.Credential -ArgumentList $server, $name + $credential.MappedClassType = $mappedclass + $credential.ProviderName = $ProviderName + $credential.Create($Identity, $Password) + + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $credential -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + + Select-DefaultView -InputObject $credential -Property ComputerName, InstanceName, SqlInstance, Name, Identity, CreateDate, MappedClassType, ProviderName + } + catch { + Stop-Function -Message "Failed to create credential in $cred on $instance. Exception: $($_.Exception.InnerException)" -Target $credential -InnerErrorRecord $_ -Continue + } + } + } + } + } +} diff --git a/functions/New-DbaDatabaseMasterKey.ps1 b/functions/New-DbaDatabaseMasterKey.ps1 new file mode 100644 index 000000000000..fb2c107e141c --- /dev/null +++ b/functions/New-DbaDatabaseMasterKey.ps1 @@ -0,0 +1,104 @@ +function New-DbaDatabaseMasterKey { + <# +.SYNOPSIS +Creates a new database master key + +.DESCRIPTION +Creates a new database master key. If no database is specified, the master key will be created in master. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the master key will be created. Defaults to master. + +.PARAMETER Password +Secure string used to create the key. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaDatabaseMasterKey -SqlInstance Server1 + +You will be prompted to securely enter your password, then a master key will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a master key in the 'db1' database + + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database = "master", + [parameter(Mandatory)] + [Security.SecureString]$Password = (Read-Host "Password" -AsSecureString), + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + $smodb = $server.Databases[$db] + + if ($null -eq $smodb) { + Stop-Function -Message "Database '$db' does not exist on $instance" -Target $smodb -Continue + } + + if ($null -ne $smodb.MasterKey) { + Stop-Function -Message "Master key already exists in the $db database on $instance" -Target $smodb -Continue + } + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating master key for database '$db' on $instance")) { + try { + $masterkey = New-Object Microsoft.SqlServer.Management.Smo.MasterKey $smodb + $masterkey.Create(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)))) + + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $masterkey -MemberType NoteProperty -Name Database -value $smodb + + Select-DefaultView -InputObject $masterkey -Property ComputerName, InstanceName, SqlInstance, Database, CreateDate, DateLastModified, IsEncryptedByServer + } + catch { + Stop-Function -Message "Failed to create master key in $db on $instance. Exception: $($_.Exception.InnerException)" -Target $masterkey -InnerErrorRecord $_ -Continue + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaDatabaseSnapshot.ps1 b/functions/New-DbaDatabaseSnapshot.ps1 deleted file mode 100644 index 3b76d4a62d46..000000000000 --- a/functions/New-DbaDatabaseSnapshot.ps1 +++ /dev/null @@ -1,390 +0,0 @@ -Function New-DbaDatabaseSnapshot -{ -<# -.SYNOPSIS -Creates database snapshots - -.DESCRIPTION -Creates database snapshots without hassles - -.PARAMETER SqlInstance -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER AllDatabases -Creates snapshot for all eligible databases - -.PARAMETER Databases -Creates snapshot for only specific databases - -.PARAMETER Name -When you pass a simple string, it'll be appended to use it to build the name of the snapshot. By default snapshots are created with yyyyMMdd_HHmmss suffix -You can also pass a standard placeholder, in which case it'll be interpolated (e.g. '{0}' gets replaced with the database name) - -.PARAMETER Path -Snapshot files will be created here (by default the filestructure will be created in the same folder as the base db) - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER Force -Databases with Filestream FG can be snapshotted, but the Filestream FG is marked offline -in the snapshot. To create a "partial" snapshot, you need to pass -Force explicitely - -NB: You can't then restore the Database from the newly-created snapshot. -For details, check https://msdn.microsoft.com/en-us/library/bb895334.aspx - -.NOTES -Tags: DisasterRecovery, Snapshot, Restore -Author: niphlod - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK - https://dbatools.io/New-DbaDatabaseSnapshot - -.EXAMPLE -New-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Database HR, Accounting - -Creates snapshot for HR and Accounting, returning a custom object displaying Server, Database, DatabaseCreated, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes - -.EXAMPLE -New-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Databases HR -Name _snap - -Creates snapshot named "HR_snap" for HR - -.EXAMPLE -New-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Databases HR -Name 'fool_{0}_snap' - -Creates snapshot named "fool_HR_snap" for HR - -.EXAMPLE -New-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Databases HR, Accounting -Path F:\snapshotpath - -Creates snapshots for HR and Accounting databases, storing files under the F:\snapshotpath\ dir - -#> - - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [string[]]$SqlInstance, - [PsCredential]$Credential, - [switch]$AllDatabases, - [string]$Name, - [string]$Path, - [switch]$Force - ) - - DynamicParam - { - if ($SqlInstance) - { - return Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $Credential - } - } - - BEGIN - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - - $NoSupportForSnap = @('model', 'master', 'tempdb') - # Evaluate the default suffix here for naming consistency - $DefaultSuffix = (Get-Date -Format "yyyyMMdd_HHmmss") - if ($Name.Length -gt 0) - { - #Validate if Name can be interpolated - try - { - $null = $Name -f 'some_string' - } - catch - { - throw "Name parameter must be a template only containing one parameter {0}" - } - - } - - function Resolve-SnapshotError($server) - { - $errhelp = '' - $CurrentEdition = $server.Edition.toLower() - $CurrentVersion = $server.Version.Major * 1000000 + $server.Version.Minor * 10000 + $server.Version.Build - if ($server.Version.Major -lt 9) - { - $errhelp = 'Not supported before 2005' - } - if ($CurrentVersion -lt 12002000 -and $errhelp.Length -eq 0) - { - if ($CurrentEdition -notmatch '.*enterprise.*|.*developer.*|.*datacenter.*') - { - $errhelp = 'Supported only for Enterprise, Developer or Datacenter editions' - } - } - $message = "" - if ($errhelp.Length -gt 0) - { - $message += "Please make sure your version supports snapshots : ($errhelp)" - } - else - { - $message += "This module can't tell you why the snapshot creation failed. Feel free to report back to dbatools what happened" - } - Write-Warning $message - } - - } - - - PROCESS - { - if ($databases.Length -eq 0 -and $AllDatabases -eq $false -and !$smodatabase) - { - throw "You must specify a -AllDatabases or -Database to continue" - } - - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - } - catch - { - Write-Warning "Can't connect to $instance" - Continue - } - #Checks for path existence - if ($Path.Length -gt 0) - { - if (!(Test-SqlPath -SqlServer $instance -Path $Path)) - { - Write-Warning "'$instance' cannot access the directory '$Path'" - Continue - } - } - - if ($AllDatabases) - { - $dbs = $server.Databases - } - - if ($databases.count -gt 0) - { - $dbs = $server.Databases | Where-Object { $databases -contains $_.Name } - } - - $sourcedbs = @() - - ## double check for gotchas - foreach ($db in $dbs) - { - if ($db.IsDatabaseSnapshot) - { - Write-Warning "'$($db.name)' is a snapshot, skipping" - } - elseif ($db.name -in $NoSupportForSnap) - { - Write-Warning "'$($db.name)' snapshots are prohibited" - } - else - { - $sourcedbs += $db - } - } - - foreach ($db in $sourcedbs) - { - if ($Name.Length -gt 0) - { - $SnapName = $Name -f $db.Name - if ($SnapName -eq $Name) - { - #no interpolation, just append - $SnapName = '{0}{1}' -f $db.Name, $Name - } - } - else - { - $SnapName = "{0}_{1}" -f $db.Name, $DefaultSuffix - } - if ($SnapName -in $server.Databases.Name) - { - Write-Warning "A database named '$Snapname' already exists, skipping" - Continue - } - $all_FSD = $db.FileGroups | Where-Object FileGroupType -eq 'FileStreamDataFileGroup' - $all_MMO = $db.FileGroups | Where-Object FileGroupType -eq 'MemoryOptimizedDataFileGroup' - $has_FSD = $all_FSD.Count -gt 0 - $has_MMO = $all_MMO.Count -gt 0 - if ($has_MMO) - { - Write-Warning "MEMORY_OPTIMIZED_DATA detected, snapshots are not possible" - Continue - } - if ($has_FSD -and $Force -eq $false) - { - Write-Warning "Filestream detected, skipping. You need to specify -Force. See Get-Help for details" - Continue - } - $snaptype = "db snapshot" - if ($has_FSD) - { - $snaptype = "partial db snapshot" - } - If ($Pscmdlet.ShouldProcess($instance, "Create $snaptype '$SnapName' of '$($db.Name)'")) - { - $CustomFileStructure = @{ } - $counter = 0 - foreach ($fg in $db.FileGroups) - { - $CustomFileStructure[$fg.Name] = @() - if ($fg.FileGroupType -eq 'FileStreamDataFileGroup') - { - Continue - } - foreach ($file in $fg.Files) - { - $counter += 1 - # fixed extension is hardcoded as "ss", which seems a "de-facto" standard - $fname = [IO.Path]::ChangeExtension($file.Filename, "ss") - $fname = [IO.Path]::Combine((Split-Path $fname -Parent), ("{0}_{1}" -f $DefaultSuffix, (Split-Path $fname -Leaf))) - - # change path if specified - if ($Path.Length -gt 0) - { - $basename = Split-Path $fname -Leaf - # we need to avoid cases where basename is the same for multiple FG - $basename = '{0:0000}_{1}' -f $counter, $basename - $fname = [IO.Path]::Combine($Path, $basename) - } - $CustomFileStructure[$fg.Name] += @{ 'name' = $file.name; 'filename' = $fname } - } - } - $SnapDB = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Database -ArgumentList $instance, $Snapname - $SnapDB.DatabaseSnapshotBaseName = $db.Name - foreach ($fg in $CustomFileStructure.Keys) - { - $SnapFG = New-Object -TypeName Microsoft.SqlServer.Management.Smo.FileGroup $SnapDB, $fg - $SnapDB.FileGroups.Add($SnapFG) - foreach ($file in $CustomFileStructure[$fg]) - { - $SnapFile = New-Object -TypeName Microsoft.SqlServer.Management.Smo.DataFile $SnapFG, $file['name'], $file['filename'] - $SnapDB.FileGroups[$fg].Files.Add($SnapFile) - } - } - - # we're ready to issue a Create, but SMO is a little uncooperative here - # there are cases we can manage and others we can't, and we need all the - # info we can get both from testers and from users - - $sql = $SnapDB.Script() - try - { - $SnapDB.Create() - - [PSCustomObject]@{ - Server = $server.name - Database = $SnapDB.Name - SnapshotOf = $SnapDB.DatabaseSnapshotBaseName - SizeMB = [Math]::Round($SnapDB.Size, 2) - DatabaseCreated = $SnapDB.createDate - PrimaryFilePath = $SnapDB.PrimaryFilePath - Status = 'Created' - Notes = $null - SnapshotDb = $SnapDB - } | Select-DefaultView -Property Server, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status - } - catch - { - $Status = 'Created' - # here we manage issues we DO know about, hoping that the first command - # is always the one creating the snapshot, and give an helpful hint - try - { - $server.Databases.Refresh() - if ($SnapName -notin $server.Databases.Name) - { - # previous creation failed completely, snapshot is not there already - $null = $server.ConnectionContext.ExecuteNonQuery($sql[0]) - $server.Databases.Refresh() - $SnapDB = $server.Databases[$Snapname] - } - else - { - $SnapDB = $server.Databases[$Snapname] - } - $Notes = @() - if ($db.ReadOnly -eq $true) - { - $Notes += 'SMO is probably trying to set a property on a read-only snapshot, run with -Debug to find out and report back' - } - if ($has_FSD) - { - $Status = 'Partial' - $Notes += 'Filestream groups are not viable for snapshot' - } - $Notes = $Notes -Join ';' - - $hints = @("Executing these commands led to a partial failure") - foreach ($stmt in $sql) - { - $hints += $stmt - } - Write-Debug ($hints -Join "`n") - - [PSCustomObject]@{ - Server = $server.name - Database = $SnapDB.Name - SnapshotOf = $SnapDB.DatabaseSnapshotBaseName - SizeMB = [Math]::Round($SnapDB.Size, 2) - DatabaseCreated = $SnapDB.createDate - PrimaryFilePath = $SnapDB.PrimaryFilePath - Status = $Status - Notes = $Notes - SnapshotDb = $SnapDB - } | Select-DefaultView -Property Server, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes - } - catch - { - # we end up here when even the first issued command didn't create - # a valid snapshot - $ex = $_ - Write-Warning 'SMO failed to create the snapshot, run with -Debug to find out and report back' - $hints = @("Executing these commands led to a failure") - foreach ($stmt in $sql) - { - $hints += $stmt - } - Write-Exception $ex - $inner = $_.Exception.Message - if ($null -ne $ex.Exception.InnerException) - { - $inner = $ex.Exception.InnerException - } - if ($null -ne $inner.InnerException) - { - $inner = $inner.InnerException - } - Write-Warning "Original exception: $inner" - Write-Debug ($hints -Join "`n") - Resolve-SnapshotError $server - } - } - } - } - } - } -} diff --git a/functions/New-DbaDbCertificate.ps1 b/functions/New-DbaDbCertificate.ps1 new file mode 100644 index 000000000000..52642872d8ab --- /dev/null +++ b/functions/New-DbaDbCertificate.ps1 @@ -0,0 +1,154 @@ +function New-DbaDbCertificate { + <# +.SYNOPSIS +Creates a new database certificate + +.DESCRIPTION +Creates a new database certificate. If no database is specified, the certificate will be created in master. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the certificate will be created. Defaults to master. + +.PARAMETER Name +Optional name to create the certificate. Defaults to database name. + +.PARAMETER Subject +Optional subject to create the certificate. + +.PARAMETER StartDate +Optional secure string used to create the certificate. + +.PARAMETER ExpirationDate +Optional secure string used to create the certificate. + +.PARAMETER ActiveForServiceBrokerDialog +Optional secure string used to create the certificate. + +.PARAMETER Password +Optional password - if no password is supplied, the password will be protected by the master key + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaDbCertificate -SqlInstance Server1 + +You will be prompted to securely enter your password, then a certificate will be created in the master database on server1 if it does not exist. + +.EXAMPLE +New-DbaDbCertificate -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to install but prompts to securely enter your password and creates a certificate in the 'db1' database +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string[]]$Name, + [object[]]$Database = "master", + [string[]]$Subject, + [datetime]$StartDate = (Get-Date), + [datetime]$ExpirationDate = $StartDate.AddYears(5), + [switch]$ActiveForServiceBrokerDialog, + [Security.SecureString]$Password, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias New-DbaDatabaseCertificate + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + + $currentdb = $server.Databases[$db] | Where-Object IsAccessible + + if ($null -eq $currentdb) { + Stop-Function -Message "Database '$db' does not exist on $instance" -Target $server -Continue + } + + if ($null -eq $name) { + Write-Message -Level Verbose -Message "Name is NULL, setting it to '$db'" + $name = $db + } + if ($null -eq $subject) { + Write-Message -Level Verbose -Message "Subject is NULL, setting it to '$db Database Certificate'" + $subject = "$db Database Certificate" + } + + foreach ($cert in $name) { + if ($null -ne $currentdb.Certificates[$cert]) { + Stop-Function -Message "Certificate '$cert' already exists in the $db database on $instance" -Target $currentdb -Continue + } + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating certificate for database '$db' on $instance")) { + try { + $smocert = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Certificate $currentdb, $cert + + $smocert.StartDate = $StartDate + $smocert.Subject = $Subject + $smocert.ExpirationDate = $ExpirationDate + $smocert.ActiveForServiceBrokerDialog = $ActiveForServiceBrokerDialog + + if ($password.Length -gt 0) { + $smocert.Create(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)))) + } + else { + $smocert.Create() + } + + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name ComputerName -value $server.NetName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name InstanceName -value $server.ServiceName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name SqlInstance -value $server.DomainInstanceName + Add-Member -Force -InputObject $smocert -MemberType NoteProperty -Name Database -value $currentdb.Name + + Select-DefaultView -InputObject $smocert -Property ComputerName, InstanceName, SqlInstance, Database, Name, Subject, StartDate, ActiveForServiceBrokerDialog, ExpirationDate, Issuer, LastBackupDate, Owner, PrivateKeyEncryptionType, Serial + } + catch { + if ($_.Exception.InnerException) { + $exception = $_.Exception.InnerException.ToString() -Split "System.Data.SqlClient.SqlException: " + $exception = ($exception[1] -Split "at Microsoft.SqlServer.Management.Common.ConnectionManager")[0] + } + else { + $exception = $_.Exception + } + + Stop-Function -Message "Failed to create certificate in $db on $instance. Exception: $exception" -Target $smocert -InnerErrorRecord $_ -Continue + } + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaDbSnapshot.ps1 b/functions/New-DbaDbSnapshot.ps1 new file mode 100644 index 000000000000..c9994a3f56a8 --- /dev/null +++ b/functions/New-DbaDbSnapshot.ps1 @@ -0,0 +1,352 @@ +#ValidationTags#FlowControl# +function New-DbaDbSnapshot { + <# + .SYNOPSIS + Creates database snapshots + + .DESCRIPTION + Creates database snapshots without hassles + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER AllDatabases + Creates snapshot for all eligible databases + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER Name + The specific snapshot name you want to create. Works only if you target a single database. If you need to create multiple snapshot, + you must use the NameSuffix parameter + + .PARAMETER NameSuffix + When you pass a simple string, it'll be appended to use it to build the name of the snapshot. By default snapshots are created with yyyyMMdd_HHmmss suffix + You can also pass a standard placeholder, in which case it'll be interpolated (e.g. '{0}' gets replaced with the database name) + + .PARAMETER Path + Snapshot files will be created here (by default the filestructure will be created in the same folder as the base db) + + .PARAMETER Force + Databases with Filestream FG can be snapshotted, but the Filestream FG is marked offline + in the snapshot. To create a "partial" snapshot, you need to pass -Force explicitely + + NB: You can't then restore the Database from the newly-created snapshot. + For details, check https://msdn.microsoft.com/en-us/library/bb895334.aspx + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Restore, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaDbSnapshot + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Creates snapshot for HR and Accounting, returning a custom object displaying Server, Database, DatabaseCreated, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -Name HR_snap + + Creates snapshot named "HR_snap" for HR + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -NameSuffix 'fool_{0}_snap' + + Creates snapshot named "fool_HR_snap" for HR + + .EXAMPLE + New-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting -Path F:\snapshotpath + + Creates snapshots for HR and Accounting databases, storing files under the F:\snapshotpath\ dir + +#> + + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [string]$Name, + [string]$NameSuffix, + [string]$Path, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + $NoSupportForSnap = @('model', 'master', 'tempdb') + # Evaluate the default suffix here for naming consistency + $DefaultSuffix = (Get-Date -Format "yyyyMMdd_HHmmss") + if ($NameSuffix.Length -gt 0) { + #Validate if Name can be interpolated + try { + $null = $NameSuffix -f 'some_string' + } + catch { + Stop-Function -Message "NameSuffix parameter must be a template only containing one parameter {0}" -ErrorRecord $_ + } + } + + function Resolve-SnapshotError($server) { + $errhelp = '' + $CurrentEdition = $server.Edition.toLower() + $CurrentVersion = $server.Version.Major * 1000000 + $server.Version.Minor * 10000 + $server.Version.Build + if ($server.Version.Major -lt 9) { + $errhelp = 'Not supported before 2005' + } + if ($CurrentVersion -lt 12002000 -and $errhelp.Length -eq 0) { + if ($CurrentEdition -notmatch '.*enterprise.*|.*developer.*|.*datacenter.*') { + $errhelp = 'Supported only for Enterprise, Developer or Datacenter editions' + } + } + $message = "" + if ($errhelp.Length -gt 0) { + $message += "Please make sure your version supports snapshots : ($errhelp)" + } + else { + $message += "This module can't tell you why the snapshot creation failed. Feel free to report back to dbatools what happened" + } + Write-Message -Level Warning -Message $message + } + } + process { + if (!$Database -and $AllDatabases -eq $false) { + Stop-Function -Message "You must specify a -AllDatabases or -Database to continue" -EnableException $EnableException + return + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + #Checks for path existence + if ($Path.Length -gt 0) { + if (!(Test-DbaSqlPath -SqlInstance $instance -Path $Path)) { + Stop-Function -Message "$instance cannot access the directory $Path" -ErrorRecord $_ -Target $instance -Continue -EnableException $EnableException + } + } + + if ($AllDatabases) { + $dbs = $server.Databases + } + + if ($Database) { + $dbs = $server.Databases | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $server.Databases | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + + $sourcedbs = @() + + ## double check for gotchas + foreach ($db in $dbs) { + if ($db.IsDatabaseSnapshot) { + Write-Message -Level Warning -Message "$($db.name) is a snapshot, skipping" + } + elseif ($db.name -in $NoSupportForSnap) { + Write-Message -Level Warning -Message "$($db.name) snapshots are prohibited" + } + elseif ($db.IsAccessible -ne $true) { + Write-Message -Level Verbose -Message "$($db.name) is not accessible, skipping" + } + else { + $sourcedbs += $db + } + } + if ($sourcedbs.Length -gt 1 -and $Name) { + Stop-Function -Message "You passed the Name parameter that is fixed but selected multiple databases to snapshot: use the NameSuffix parameter" -Continue -EnableException $EnableException + } + foreach ($db in $sourcedbs) { + if ($NameSuffix.Length -gt 0) { + $SnapName = $NameSuffix -f $db.Name + if ($SnapName -eq $NameSuffix) { + #no interpolation, just append + $SnapName = '{0}{1}' -f $db.Name, $NameSuffix + } + } + elseif ($Name.Length -gt 0) { + $SnapName = $Name + } + else { + $SnapName = "{0}_{1}" -f $db.Name, $DefaultSuffix + } + if ($SnapName -in $server.Databases.Name) { + Write-Message -Level Warning -Message "A database named $Snapname already exists, skipping" + continue + } + $all_FSD = $db.FileGroups | Where-Object FileGroupType -eq 'FileStreamDataFileGroup' + $all_MMO = $db.FileGroups | Where-Object FileGroupType -eq 'MemoryOptimizedDataFileGroup' + $has_FSD = $all_FSD.Count -gt 0 + $has_MMO = $all_MMO.Count -gt 0 + if ($has_MMO) { + Write-Message -Level Warning -Message "MEMORY_OPTIMIZED_DATA detected, snapshots are not possible" + continue + } + if ($has_FSD -and $Force -eq $false) { + Write-Message -Level Warning -Message "Filestream detected, skipping. You need to specify -Force. See Get-Help for details" + continue + } + $snaptype = "db snapshot" + if ($has_FSD) { + $snaptype = "partial db snapshot" + } + If ($Pscmdlet.ShouldProcess($instance, "Create $snaptype $SnapName of $($db.Name)")) { + $CustomFileStructure = @{ } + $counter = 0 + foreach ($fg in $db.FileGroups) { + $CustomFileStructure[$fg.Name] = @() + if ($fg.FileGroupType -eq 'FileStreamDataFileGroup') { + Continue + } + foreach ($file in $fg.Files) { + $counter += 1 + $basename = [IO.Path]::GetFileNameWithoutExtension($file.FileName) + $basepath = Split-Path $file.FileName -Parent + # change path if specified + if ($Path.Length -gt 0) { + $basepath = $Path + } + # we need to avoid cases where basename is the same for multiple FG + $fname = [IO.Path]::Combine($basepath, ("{0}_{1}_{2:0000}_{3:000}" -f $basename, $DefaultSuffix, (Get-Date).MilliSecond, $counter)) + # fixed extension is hardcoded as "ss", which seems a "de-facto" standard + $fname = [IO.Path]::ChangeExtension($fname, "ss") + $CustomFileStructure[$fg.Name] += @{ 'name' = $file.name; 'filename' = $fname } + } + } + $SnapDB = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Database -ArgumentList $server, $Snapname + $SnapDB.DatabaseSnapshotBaseName = $db.Name + foreach ($fg in $CustomFileStructure.Keys) { + $SnapFG = New-Object -TypeName Microsoft.SqlServer.Management.Smo.FileGroup $SnapDB, $fg + $SnapDB.FileGroups.Add($SnapFG) + foreach ($file in $CustomFileStructure[$fg]) { + $SnapFile = New-Object -TypeName Microsoft.SqlServer.Management.Smo.DataFile $SnapFG, $file['name'], $file['filename'] + $SnapDB.FileGroups[$fg].Files.Add($SnapFile) + } + } + + # we're ready to issue a Create, but SMO is a little uncooperative here + # there are cases we can manage and others we can't, and we need all the + # info we can get both from testers and from users + + $sql = $SnapDB.Script() + try { + $SnapDB.Create() + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $SnapDB.Name + SnapshotOf = $SnapDB.DatabaseSnapshotBaseName + SizeMB = [Math]::Round($SnapDB.Size, 2) ##FIXME, should use the stats for sparse files + DatabaseCreated = [dbadatetime]$SnapDB.createDate + PrimaryFilePath = $SnapDB.PrimaryFilePath + Status = 'Created' + Notes = $null + SnapshotDb = $SnapDB + } | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status + } + catch { + $Status = 'Created' + # here we manage issues we DO know about, hoping that the first command + # is always the one creating the snapshot, and give an helpful hint + try { + $server.Databases.Refresh() + if ($SnapName -notin $server.Databases.Name) { + # previous creation failed completely, snapshot is not there already + $null = $server.Query($sql[0]) + $server.Databases.Refresh() + $SnapDB = $server.Databases[$Snapname] + } + else { + $SnapDB = $server.Databases[$Snapname] + } + $Notes = @() + if ($db.ReadOnly -eq $true) { + $Notes += 'SMO is probably trying to set a property on a read-only snapshot, run with -Debug to find out and report back' + } + if ($has_FSD) { + $Status = 'Partial' + $Notes += 'Filestream groups are not viable for snapshot' + } + $Notes = $Notes -Join ';' + + $hints = @("Executing these commands led to a partial failure") + foreach ($stmt in $sql) { + $hints += $stmt + } + Write-Message -Level Debug -Message ($hints -Join "`n") + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $SnapDB.Name + SnapshotOf = $SnapDB.DatabaseSnapshotBaseName + SizeMB = [Math]::Round($SnapDB.Size, 2) ##FIXME, should use the stats for sparse files + DatabaseCreated = [dbadatetime]$SnapDB.createDate + PrimaryFilePath = $SnapDB.PrimaryFilePath + Status = $Status + Notes = $Notes + SnapshotDb = $SnapDB + } | Select-DefaultView -Property ComputerName, InstanceName, SqlInstance, Database, SnapshotOf, SizeMB, DatabaseCreated, PrimaryFilePath, Status, Notes + } + catch { + # we end up here when even the first issued command didn't create + # a valid snapshot + Write-Message -Level Warning -Message 'SMO failed to create the snapshot, run with -Debug to find out and report back' -ErrorRecord $_ + $hints = @("Executing these commands led to a failure") + foreach ($stmt in $sql) { + $hints += $stmt + } + Write-Message -Level Debug -Message ($hints -Join "`n") + Resolve-SnapshotError $server + } + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias New-DbaDatabaseSnapshot + } +} + diff --git a/functions/New-DbaDbUser.ps1 b/functions/New-DbaDbUser.ps1 new file mode 100644 index 000000000000..8c4544045aa0 --- /dev/null +++ b/functions/New-DbaDbUser.ps1 @@ -0,0 +1,251 @@ +function New-DbaDbUser { + <# + .SYNOPSIS + Creates a new user for the specified database. + + .DESCRIPTION + Creates a new user for a specified database with provided specifications. + + .PARAMETER SqlInstance + The target SQL Server instance. Defaults to the default instance on localhost. + + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins instead of Windows Authentication (AKA Integrated or Trusted). To use: + + $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + + Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. + + To connect to SQL Server as a different Windows user, run PowerShell as that user. + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. By default, system databases are excluded. + + .PARAMETER IncludeSystem + If this switch is enabled, the user will be added to system databases. + + .PARAMETER Login + When specified, the user will be associated to this SQL login and have the same name as the Login. + + .PARAMETER Username + When specified, the user will have this name. + + .PARAMETER Force + If user exists, drop and recreate. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, User + Author: Frank Henninger (@osiris687) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaDbUser + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Login user1 + + Creates a new sql user with login named user1 in the specified database. + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Username user1 + + Creates a new sql user without login named user1 in the specified database. + + .EXAMPLE + New-DbaDbUser -SqlInstance sqlserver2014 -Database DB1 -Login Login1 -Username user1 + + Creates a new sql user named user1 mapped to Login1 in the specified database. + + .EXAMPLE + Get-DbaDatabaseUser -SqlInstance sqlserver1 -Database DB1 | New-DbaDbUser -SqlInstance sqlserver2 -Database DB1 + + Copies users from sqlserver1.DB1 to sqlserver2.DB1. Does not copy permissions! + #> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "NoLogin")] + param( + [parameter(Mandatory, Position = 1)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$IncludeSystem, + [parameter(ParameterSetName = "Login")] + [string[]]$Login, + [parameter(ParameterSetName = "NoLogin")] + [parameter(ParameterSetName = "Login")] + [string[]]$Username, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Test-SqlLoginInDatabase { + param( + [Microsoft.SqlServer.Management.Smo.Login]$Login, + [Microsoft.SqlServer.Management.Smo.Database]$Database + ) + + # Does user exist with same login? + if ( $existingUser = ( $Database.Users | Where-Object Login -eq $smoLogin ) ) { + if (Test-Bound 'Force') { + if ($Pscmdlet.ShouldProcess($existingUser, "Dropping existing user $($existingUser.Name) because -Force was used")) { + try { + $existingUser.Drop() + } + catch { + Stop-Function -Message "Could not remove existing user $($existingUser.Name), skipping." -Target $existingUser -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + } + else { + Stop-Function -Message "User $($existingUser.Name) already exists and -Force was not specified" -Target $existingUser -Continue + } + } + } + + function Test-SqlUserInDatabase { + param( + [string[]]$Username, + [Microsoft.SqlServer.Management.Smo.Database]$Database + ) + + # Does user exist with same login? + if ( $existingUser = ( $Database.Users | Where-Object Name -eq $Username ) ) { + if (Test-Bound 'Force') { + if ($Pscmdlet.ShouldProcess($existingUser, "Dropping existing user $($existingUser.Name) because -Force was used")) { + try { + $existingUser.Drop() + } + catch { + Stop-Function -Message "Could not remove existing user $($existingUser.Name), skipping." -Target $existingUser -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + } + else { + Stop-Function -Message "User $($existingUser.Name) already exists and -Force was not specified" -Target $existingUser -Continue + } + } + } + } + + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible -eq $true + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + if (Test-Bound 'IncludeSystem' -Not) { + $databases = $databases | Where-Object IsSystemObject -NE $true + } + + foreach ($db in $databases) { + Write-Message -Level Verbose -Message "Add users to Database $db on target $server" + + switch -Wildcard ($PSCmdlet.ParameterSetName) { + "Login*" { + # Creates a user with Login + Write-Message -Level VeryVerbose -Message "Using UserType: SqlLogin" + + if ($PSBoundParameters.Keys -notcontains 'Login') { + Stop-Function -Message "Parameter -Login is required " -Target $instance + } + if ($Login.GetType().Name -eq 'Login') { + $smoLogin = $Login + } + else { + #get the login associated with the given name. + $smoLogin = $server.Logins | Where-Object Name -eq $Login + if ($smoLogin -eq $null) { + Stop-Function -Message "Invalid Login: $Login is not found on $Server" -Target $instance; + return + } + } + + Test-SqlLoginInDatabase -Database $db -Login $smoLogin + + if ( $PSCmdlet.ParameterSetName -eq "LoginWithNewUsername" ) { + $Name = $Username + Write-Message -Level Verbose -Message "Using UserName: $Username" + } + else { + $Name = $smoLogin.Name + Write-Message -Level Verbose -Message "Using LoginName: $Name" + } + + $Login = $smoLogin + $UserType = [Microsoft.SqlServer.Management.Smo.UserType]::SqlLogin + } + + "NoLogin" { + # Creates a user without login + Write-Message -Level Verbose -Message "Using UserType: NoLogin" + $UserType = [Microsoft.SqlServer.Management.Smo.UserType]::NoLogin + $Name = $Username + } + } #switch + + # Does user exist with same name? + Test-SqlUserInDatabase -Database $db -Username $Name + + if ($Pscmdlet.ShouldProcess($db, "Creating user $Name")) { + try { + $smoUser = New-Object Microsoft.SqlServer.Management.Smo.User + $smoUser.Parent = $db + $smoUser.Name = $Name + + if ( $PSBoundParameters.Keys -contains 'Login' -and $Login.GetType().Name -eq 'Login' ) { + $smoUser.Login = Login + } + $smoUser.UserType = $UserType + + $smoUser.Create() + } + catch { + Stop-Function -Message "Failed to add user $Name in $db to $instance" -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + $smoUser.Refresh() + + if ( $PSBoundParameters.Keys -contains 'Username' -and $smoUser.Name -ne $Username ) { + $smoUser.Rename($Username) + } + + Write-Message -Level Verbose -Message "Successfully added $smoUser in $db to $instance." + } + + #Display Results + Get-DbaDatabaseUser -SqlInstance $server.Name -Database $db.Name | Where-Object name -eq $smoUser.Name + } #foreach ($db in $databases) + } #foreach ($instance in $SqlInstance) + } +} diff --git a/functions/New-DbaLogin.ps1 b/functions/New-DbaLogin.ps1 new file mode 100644 index 000000000000..67e0162c6424 --- /dev/null +++ b/functions/New-DbaLogin.ps1 @@ -0,0 +1,473 @@ +function New-DbaLogin { + <# + .SYNOPSIS + Creates a new SQL Server login + + .DESCRIPTION + Creates a new SQL Server login with provided specifications + + .PARAMETER SqlInstance + The target SQL Server(s) + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials + + .PARAMETER Login + The Login name(s) + + .PARAMETER Password + Secure string used to authenticate the Login + + .PARAMETER HashedPassword + Hashed password string used to authenticate the Login + + .PARAMETER InputObject + Takes the parameters required from a Login object that has been piped into the command + + .PARAMETER LoginRenameHashtable + Pass a hash table into this parameter to change login names when piping objects into the procedure + + .PARAMETER MapToCertificate + Map the login to a certificate + + .PARAMETER MapToAsymmetricKey + Map the login to an asymmetric key + + .PARAMETER MapToCredential + Map the login to a credential + + .PARAMETER Sid + Provide an explicit Sid that should be used when creating the account. Can be [byte[]] or hex [string] ('0xFFFF...') + + .PARAMETER DefaultDatabase + Default database for the login + + .PARAMETER Language + Login's default language + + .PARAMETER PasswordExpiration + Enforces password expiration policy. Requires PasswordPolicy to be enabled. Can be $true or $false(default) + + .PARAMETER PasswordPolicy + Enforces password complexity policy. Can be $true or $false(default) + + .PARAMETER Disabled + Create the login in a disabled state + + .PARAMETER NewSid + Ignore sids from the piped login object to generate new sids on the server. Useful when copying login onto the same server + + .PARAMETER Force + If login exists, drop and recreate + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Login + Author: Kirill Kravtsov (@nvarscar) + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaLogin + + .EXAMPLE + New-DbaLogin -SqlInstance Server1,Server2 -Login Newlogin + + You will be prompted to securely enter the password for a login [Newlogin]. The login would be created on servers Server1 and Server2 with default parameters. + + .EXAMPLE + $securePassword = Read-Host "Input password" -AsSecureString + New-DbaLogin -SqlInstance Server1\sql1 -Login Newlogin -Password $securePassword -PasswordPolicy -PasswordExpiration + + Creates a login on Server1\sql1 with a predefined password. The login will have password and expiration policies enforced onto it. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql1 -Login Oldlogin | New-DbaLogin -SqlInstance sql1 -LoginRenameHashtable @{Oldlogin = 'Newlogin'} -Force -NewSid -Disabled:$false + + Copies a login [Oldlogin] to the same instance sql1 with the same parameters (including password). New login will have a new sid, a new name [Newlogin] and will not be disabled. Existing login [Newlogin] will be removed prior to creation. + + .EXAMPLE + Get-DbaLogin -SqlInstance sql1 -Login Login1,Login2 | New-DbaLogin -SqlInstance sql2 -PasswordPolicy -PasswordExpiration -DefaultDatabase tempdb -Disabled + + Copies logins [Login1] and [Login2] from instance sql1 to instance sql2, but enforces password and expiration policies for the new logins. New logins will also have a default database set to [tempdb] and will be created in a disabled state. +#> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Password")] + param ( + [parameter(Mandatory, Position = 1)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Name", "LoginName")] + [parameter(ParameterSetName = "Password", Position = 2)] + [parameter(ParameterSetName = "PasswordHash")] + [parameter(ParameterSetName = "MapToCertificate")] + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [string[]]$Login, + [parameter(ValueFromPipeline = $true)] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [parameter(ParameterSetName = "MapToCertificate")] + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [object[]]$InputObject, + [Alias("Rename")] + [hashtable]$LoginRenameHashtable, + [parameter(ParameterSetName = "Password", Position = 3)] + [Security.SecureString]$Password, + [Alias("Hash", "PasswordHash")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$HashedPassword, + [parameter(ParameterSetName = "MapToCertificate")] + [string]$MapToCertificate, + [parameter(ParameterSetName = "MapToAsymmetricKey")] + [string]$MapToAsymmetricKey, + [string]$MapToCredential, + [object]$Sid, + [Alias("DefaulDB")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$DefaultDatabase, + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [string]$Language, + [Alias("Expiration", "CheckExpiration")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [switch]$PasswordExpiration, + [Alias("Policy", "CheckPolicy")] + [parameter(ParameterSetName = "Password")] + [parameter(ParameterSetName = "PasswordHash")] + [switch]$PasswordPolicy, + [Alias("Disable")] + [switch]$Disabled, + [switch]$NewSid, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($Sid) { + if ($Sid.GetType().Name -ne 'Byte[]') { + foreach ($symbol in $Sid.TrimStart("0x").ToCharArray()) { + if ($symbol -notin "0123456789ABCDEF".ToCharArray()) { + Stop-Function -Message "Sid has invalid character '$symbol', cannot proceed." -Category InvalidArgument -EnableException $EnableException + return + } + } + $Sid = Convert-HexStringToByte $Sid + } + } + + if ($HashedPassword) { + if ($HashedPassword.GetType().Name -eq 'Byte[]') { + $HashedPassword = Convert-ByteToHexString $HashedPassword + } + } + } + + process { + #At least one of those should be specified + if (!($Login -or $InputObject)) { + Stop-Function -Message "No logins have been specified." -Category InvalidArgument -EnableException $EnableException + Return + } + + $loginCollection = @() + if ($InputObject) { + $loginCollection += $InputObject + if ($Login) { + Stop-Function -Message "Parameter -Login is not supported when processing objects from -InputObject. If you need to rename the logins, please use -LoginRenameHashtable." -Category InvalidArgument -EnableException $EnableException + Return + } + } + else { + $loginCollection += $Login + $Login | ForEach-Object { + if ($_.IndexOf('\') -eq -1 -and $PsCmdlet.ParameterSetName -like "Password*" -and !($Password -or $HashedPassword)) { + $passwordNotSpecified = $true + } + } + } + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($loginItem in $loginCollection) { + #check if $loginItem is an SMO Login object + if ($loginItem.GetType().Name -eq 'Login') { + #Get all the necessary fields + $loginName = $loginItem.Name + $loginType = $loginItem.LoginType + $currentSid = $loginItem.Sid + $currentDefaultDatabase = $loginItem.DefaultDatabase + $currentLanguage = $loginItem.Language + $currentPasswordExpiration = $loginItem.PasswordExpiration + $currentPasswordPolicyEnforced = $loginItem.PasswordPolicyEnforced + $currentDisabled = $loginItem.IsDisabled + + #Get previous password + if ($loginType -eq 'SqlLogin' -and !($Password -or $HashedPassword)) { + $sourceServer = $loginItem.Parent + switch ($sourceServer.versionMajor) { + 0 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM master.dbo.syslogins WHERE loginname='$loginName'" } + 8 { $sql = "SELECT CONVERT(VARBINARY(256),password) as hashedpass FROM dbo.syslogins WHERE name='$loginName'" } + 9 { $sql = "SELECT CONVERT(VARBINARY(256),password_hash) as hashedpass FROM sys.sql_logins where name='$loginName'" } + default { + $sql = "SELECT CAST(CONVERT(VARCHAR(256), CAST(LOGINPROPERTY(name,'PasswordHash') + AS VARBINARY(256)), 1) AS NVARCHAR(max)) AS hashedpass + FROM sys.server_principals + WHERE principal_id = $($loginItem.id)" + } + } + + try { + $hashedPass = $sourceServer.ConnectionContext.ExecuteScalar($sql) + } + catch { + $hashedPassDt = $sourceServer.Databases['master'].ExecuteWithResults($sql) + $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) + } + + if ($hashedPass.GetType().Name -ne "String") { + $hashedPass = Convert-ByteToHexString $hashedPass + } + $currentHashedPassword = $hashedPass + } + + #Get cryptography and attached credentials + if ($loginType -eq 'AsymmetricKey') { + $currentAsymmetricKey = $loginItem.AsymmetricKey + } + if ($loginType -eq 'Certificate') { + $currentCertificate = $loginItem.Certificate + } + #This method or property is accessible only while working with SQL Server 2008 or later. + if ($sourceServer.versionMajor -gt 9) { + if ($loginItem.EnumCredentials()) { + $currentCredential = $loginItem.EnumCredentials() + } + } + } + else { + $loginName = $loginItem + $currentSid = $currentDefaultDatabase = $currentLanguage = $currentPasswordExpiration = $currentAsymmetricKey = $currentCertificate = $currentCredential = $currentDisabled = $currentPasswordPolicyEnforced = $null + + if ($PsCmdlet.ParameterSetName -eq "MapToCertificate") { $loginType = 'Certificate' } + elseif ($PsCmdlet.ParameterSetName -eq "MapToAsymmetricKey") { $loginType = 'AsymmetricKey' } + elseif ($loginItem.IndexOf('\') -eq -1) { $loginType = 'SqlLogin' } + else { $loginType = 'WindowsUser' } + } + + if (($server.LoginMode -ne [Microsoft.SqlServer.Management.Smo.ServerLoginMode]::Mixed) -and ($loginType -eq 'SqlLogin')) { + Write-Message -Level Warning -Message "$instance does not have Mixed Mode enabled. [$loginName] is an SQL Login. Enable mixed mode authentication after the migration completes to use this type of login." -EnableException $EnableException + } + + if ($Sid) { + $currentSid = $Sid + } + if ($DefaultDatabase) { + $currentDefaultDatabase = $DefaultDatabase + } + if ($Language) { + $currentLanguage = $Language + } + if ($PSBoundParameters.Keys -contains 'PasswordExpiration') { + $currentPasswordExpiration = $PasswordExpiration + } + if ($PSBoundParameters.Keys -contains 'PasswordPolicy') { + $currentPasswordPolicyEnforced = $PasswordPolicy + } + if ($PSBoundParameters.Keys -contains 'MapToAsymmetricKey') { + $currentAsymmetricKey = $MapToAsymmetricKey + } + if ($PSBoundParameters.Keys -contains 'MapToCertificate') { + $currentCertificate = $MapToCertificate + } + if ($PSBoundParameters.Keys -contains 'MapToCredential') { + $currentCredential = $MapToCredential + } + if ($PSBoundParameters.Keys -contains 'Disabled') { + $currentDisabled = $Disabled + } + + #Apply renaming if necessary + if ($LoginRenameHashtable.Keys -contains $loginName) { + $loginName = $LoginRenameHashtable[$loginName] + } + + #Requesting password if required + if ($loginItem.GetType().Name -ne 'Login' -and $loginType -eq 'SqlLogin' -and !($Password -or $HashedPassword)) { + $Password = Read-Host -AsSecureString -Prompt "Enter a new password for the SQL Server login(s)" + } + + #verify if login exists on the server + if ($existingLogin = $server.Logins[$loginName]) { + if ($force) { + if ($Pscmdlet.ShouldProcess($existingLogin, "Dropping existing login $loginName on $instance because -Force was used")) { + try { + $existingLogin.Drop() + } + catch { + Stop-Function -Message "Could not remove existing login $loginName on $instance, skipping." -Target $loginName -Continue + } + } + } + else { + Stop-Function -Message "Login $loginName already exists on $instance and -Force was not specified" -Target $loginName -Continue + } + } + + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Creating login $loginName on $instance")) { + try { + $newLogin = New-Object Microsoft.SqlServer.Management.Smo.Login($server, $loginName) + $newLogin.LoginType = $loginType + + $withParams = "" + + if ($loginType -eq 'SqlLogin' -and $currentSid -and !$NewSid) { + Write-Message -Level Verbose -Message "Setting $loginName SID" + $withParams += ", SID = " + (Convert-ByteToHexString $currentSid) + $newLogin.Set_Sid($currentSid) + } + + if ($loginType -in ("WindowsUser", "WindowsGroup", "SqlLogin")) { + if ($currentDefaultDatabase) { + Write-Message -Level Verbose -Message "Setting $loginName default database to $currentDefaultDatabase" + $withParams += ", DEFAULT_DATABASE = [$currentDefaultDatabase]" + $newLogin.DefaultDatabase = $currentDefaultDatabase + } + + if ($currentLanguage) { + Write-Message -Level Verbose -Message "Setting $loginName language to $currentLanguage" + $withParams += ", DEFAULT_LANGUAGE = [$currentLanguage]" + $newLogin.Language = $currentLanguage + } + + #CHECK_EXPIRATION: default - OFF + if ($currentPasswordExpiration) { + $withParams += ", CHECK_EXPIRATION = ON" + $newLogin.PasswordExpirationEnabled = $true + } + else { + $withParams += ", CHECK_EXPIRATION = OFF" + $newLogin.PasswordExpirationEnabled = $false + } + + #CHECK_POLICY: default - ON + if ($currentPasswordPolicyEnforced) { + $withParams += ", CHECK_POLICY = ON" + $newLogin.PasswordPolicyEnforced = $true + } + else { + $withParams += ", CHECK_POLICY = OFF" + $newLogin.PasswordPolicyEnforced = $false + } + + #Generate hashed password if necessary + if ($Password) { + $currentHashedPassword = Get-PasswordHash $Password $server.versionMajor + } + elseif ($HashedPassword) { + $currentHashedPassword = $HashedPassword + } + } + elseif ($loginType -eq 'AsymmetricKey') { + $newLogin.AsymmetricKey = $currentAsymmetricKey + } + elseif ($loginType -eq 'Certificate') { + $newLogin.Certificate = $currentCertificate + } + + #Add credential + if ($currentCredential) { + $withParams += ", CREDENTIAL = [$currentCredential]" + } + + Write-Message -Level Verbose -Message "Adding as login type $loginType" + + # Attempt to add login using SMO, then T-SQL + try { + if ($loginType -in ("WindowsUser", "WindowsGroup", "AsymmetricKey", "Certificate")) { + if ($withParams) { $withParams = " WITH " + $withParams.TrimStart(',') } + $newLogin.Create() + } + elseif ($loginType -eq "SqlLogin") { + $newLogin.Create($currentHashedPassword, [Microsoft.SqlServer.Management.Smo.LoginCreateOptions]::IsHashed) + } + $newLogin.Refresh() + + #Adding credential + if ($currentCredential) { + try { + $newLogin.AddCredential($currentCredential) + } + catch { + $newLogin.Drop() + Stop-Function -Message "Failed to add $loginName to $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + Write-Message -Level Verbose -Message "Successfully added $loginName to $instance." + } + catch { + Write-Message -Level Verbose -Message "Failed to create $loginName on $instance using SMO, trying T-SQL." + try { + if ($loginType -eq 'AsymmetricKey') { $sql = "CREATE LOGIN [$loginName] FROM ASYMMETRIC KEY [$currentAsymmetricKey]" } + elseif ($loginType -eq 'Certificate') { $sql = "CREATE LOGIN [$loginName] FROM CERTIFICATE [$currentCertificate]" } + elseif ($loginType -eq "SqlLogin") { $sql = "CREATE LOGIN [$loginName] WITH PASSWORD = $currentHashedPassword HASHED" + $withParams } + else { $sql = "CREATE LOGIN [$loginName] FROM WINDOWS" + $withParams } + + $null = $server.Query($sql) + $newLogin = $server.logins[$loginName] + Write-Message -Level Verbose -Message "Successfully added $loginName to $instance." + } + catch { + Stop-Function -Message "Failed to add $loginName to $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + + #Process the Disabled property + if ($currentDisabled) { + try { + $newLogin.Disable() + Write-Message -Level Verbose -Message "Login $loginName has been disabled on $instance." + } + catch { + Write-Message -Level Verbose -Message "Failed to disable $loginName on $instance using SMO, trying T-SQL." + try { + $sql = "ALTER LOGIN [$loginName] DISABLE" + $null = $server.Query($sql) + Write-Message -Level Verbose -Message "Login $loginName has been disabled on $instance." + } + catch { + Stop-Function -Message "Failed to disable $loginName on $instance." -Category InvalidOperation -ErrorRecord $_ -Target $instance -Continue + } + } + } + #Display results + Get-DbaLogin -SqlInstance $server -Login $loginName + } + catch { + Stop-Function -Message "Failed to create login $loginName on $instance." -Target $credential -InnerErrorRecord $_ -Continue + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaPublishProfile.ps1 b/functions/New-DbaPublishProfile.ps1 new file mode 100644 index 000000000000..814efc827090 --- /dev/null +++ b/functions/New-DbaPublishProfile.ps1 @@ -0,0 +1,148 @@ +function New-DbaPublishProfile { + <# + .SYNOPSIS + Creates a new Publish Profile. + + .DESCRIPTION + The New-PublishProfile command generates a standard publish profile xml file that can be used by the DacFx (this and everything else) to control the deployment of your dacpac + This generates a standard template XML which is enough to dpeloy a dacpac but it is highly recommended that you add additional options to the publish profile. + If you use Visual Studio you can open a publish.xml file and use the ui to edit the file - + To create a new file, right click on an SSDT project, choose "Publish" then "Load Profile" and load your profile or create a new one. + Once you have loaded it in Visual Studio, clicking advanced shows you the list of options available to you. + For a full list of options that you can add to the profile, google "sqlpackage.exe command line switches" or (https://msdn.microsoft.com/en-us/library/hh550080(v=vs.103).aspx) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to and publish to. Alternatively, you can provide a ConnectionString. + + .PARAMETER SqlCredential + Allows you to login to servers using alternative logins instead Integrated, accepts Credential object created by Get-Credential + + .PARAMETER Database + The database name you are targeting + + .PARAMETER ConnectionString + The connection string to the database you are upgrading. + + Alternatively, you can provide a SqlInstance (and optionally SqlCredential) and the script will connect and generate the connectionstring. + + .PARAMETER Path + The directory where you would like to save the profile xml file(s). + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Dacpac + Author: Richie lee (@bzzzt_io) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + .LINK + https://dbatools.io/New-DbaPublishProfile + + .EXAMPLE + New-DbaPublishProfile -SqlInstance sql2017 -SqlCredential (Get-Credential) -Database WorldWideImporters -Path C:\temp + + In this example, a prompt will appear for alternative credentials, tghen a connection will be made to sql2017. Using that connection, + the ConnectionString will be extracted and used within the Publish Profile XML file which will be created at C:\temp\sql2017-WorldWideImporters-publish.xml + + .EXAMPLE + New-DbaPublishProfile -Database WorldWideImporters -Path C:\temp -ConnectionString "SERVER=(localdb)\MSSQLLocalDB;Integrated Security=True;Database=master" + + In this example, no connections are made, and a Publish Profile XML would be created at C:\temp\localdb-MSSQLLocalDB-WorldWideImporters-publish.xml + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Parameter(Mandatory)] + [string[]]$Database, + [string]$Path = "$home\Documents", + [string[]]$ConnectionString, + [switch]$EnableException + ) + begin { + if ((Test-Bound -Not -ParameterName SqlInstance) -and (Test-Bound -Not -ParameterName ConnectionString)) { + Stop-Function -Message "You must specify either SqlInstance or ConnectionString" + } + + if (-not (Test-Path $Path)) { + Stop-Function -Message "$Path doesn't exist or access denied" + } + + if ((Get-Item $path) -isnot [System.IO.DirectoryInfo]) { + Stop-Function -Message "Path must be a directory" + } + + function Get-Template ($db, $connstring) { + " + + + {0} + {1} + 1 + + " -f $db[0], $connstring + } + + function Get-ServerName ($connstring) { + $builder = New-Object System.Data.Common.DbConnectionStringBuilder + $builder.set_ConnectionString($connstring) + $instance = $builder['data source'] + + if (-not $instance) { + $instance = $builder['server'] + } + + return $instance.ToString().Replace('\', '--') + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $ConnectionString += $server.ConnectionContext.ConnectionString.Replace(';Application Name="dbatools PowerShell module - dbatools.io"', '') + + } + + foreach ($connstring in $ConnectionString) { + foreach ($db in $Database) { + $profileTemplate = Get-Template $db, $connstring + $instancename = Get-ServerName $connstring + + try { + $server = [DbaInstance]($instancename.ToString().Replace('--', '\')) + $PublishProfile = Join-Path $Path "$($instancename.Replace('--','-'))-$db-publish.xml" -ErrorAction Stop + Write-Message -Level Verbose -Message "Writing to $PublishProfile" + $profileTemplate | Out-File $PublishProfile -ErrorAction Stop + [pscustomobject]@{ + ComputerName = $server.ComputerName + InstanceName = $server.InstanceName + SqlInstance = $server.FullName + Database = $db + FileName = $PublishProfile + ConnectionString = $connstring + ProfileTemplate = $profileTemplate + } | Select-DefaultView -ExcludeProperty ComputerName, InstanceName, ProfileTemplate + } + catch { + Stop-Function -ErrorRecord $_ -Message "Failure" -Target $instancename -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaScriptingOption.ps1 b/functions/New-DbaScriptingOption.ps1 new file mode 100644 index 000000000000..2ddfb51eb271 --- /dev/null +++ b/functions/New-DbaScriptingOption.ps1 @@ -0,0 +1,32 @@ +function New-DbaScriptingOption { + <# + .SYNOPSIS + Creates a new Microsoft.SqlServer.Management.Smo.ScriptingOptions object + + .DESCRIPTION + Creates a new Microsoft.SqlServer.Management.Smo.ScriptingOptions object. Basically saves you the time from remembering the SMO assembly name ;) + + See https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.scriptingoptions.aspx for more information + + .NOTES + Tags: Migration, Backup, DR + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaScriptingOption + https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.scriptingoptions.aspx + + .EXAMPLE + $options = New-DbaScriptingOption + $options.ScriptDrops = $false + $options.WithDependencies = $true + Get-DbaAgentJob -SqlInstance sql2016 | Export-DbaScript -ScriptingOptionObject $options + + Exports Agent Jobs with the Scripting Options ScriptDrops set to $false and WithDependencies set to true + + #> + New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions +} \ No newline at end of file diff --git a/functions/New-DbaServiceMasterKey.ps1 b/functions/New-DbaServiceMasterKey.ps1 new file mode 100644 index 000000000000..f55f011e632a --- /dev/null +++ b/functions/New-DbaServiceMasterKey.ps1 @@ -0,0 +1,66 @@ +function New-DbaServiceMasterKey { + <# +.SYNOPSIS +Creates a new service master key + +.DESCRIPTION +Creates a new service master key in the master database + +.PARAMETER SqlInstance +The target SQL Server instances + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Password +Secure string used to create the key. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Certificate + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +New-DbaServiceMasterKey -SqlInstance Server1 + +You will be prompted to securely enter your Service Key Password twice, then a master key will be created in the master database on server1 if it does not exist. + +#> + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Security.SecureString]$Password, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + if (Test-Bound -ParameterName Password -Not) { + $password = Read-Host -AsSecureString -Prompt "You must enter Service Key password for $instance" + $password2 = Read-Host -AsSecureString -Prompt "Type the password again" + + if (([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) -ne ([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password2)))) { + Stop-Function -Message "Passwords do not match" -Continue + } + } + New-DbaDatabaseMasterKey -SqlInstance $instance -Database master -Password $password + } + } +} \ No newline at end of file diff --git a/functions/New-DbaSqlConnectionString.ps1 b/functions/New-DbaSqlConnectionString.ps1 new file mode 100644 index 000000000000..82223208bb2c --- /dev/null +++ b/functions/New-DbaSqlConnectionString.ps1 @@ -0,0 +1,258 @@ +function New-DbaSqlConnectionString { + <# + .SYNOPSIS + Builds or extracts a SQL Server Connection String + + .DESCRIPTION + Builds or extracts a SQL Server Connection String + + See https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.connectionstring.aspx + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnectionstringbuilder.aspx + and https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.aspx + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user be it Windows or SQL Server. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER AccessToken + Gets or sets the access token for the connection. + + .PARAMETER AppendConnectionString + Appends to the current connection string. Note that you cannot pass authentication information using this method. Use -SqlInstance and, optionally, -SqlCredential to set authentication information. + + .PARAMETER ApplicationIntent + Declares the application workload type when connecting to a server. Possible values are ReadOnly and ReadWrite. + + .PARAMETER BatchSeparator + By default, this is "GO" + + .PARAMETER ClientName + By default, this command sets the client to "dbatools PowerShell module - dbatools.io - custom connection" if you're doing anything that requires profiling, you can look for this client name. Using -ClientName allows you to set your own custom client. + + .PARAMETER ConnectTimeout + The length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error. + + Valid values are greater than or equal to 0 and less than or equal to 2147483647. + + When opening a connection to a Azure SQL Database, set the connection timeout to 30 seconds. + + .PARAMETER EncryptConnection + When true, SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed. Recognized values are true, false, yes, and no. For more information, see Connection String Syntax. + + Beginning in .NET Framework 4.5, when TrustServerCertificate is false and Encrypt is true, the server name (or IP address) in a SQL Server SSL certificate must exactly match the server name (or IP address) specified in the connection string. Otherwise, the connection attempt will fail. For information about support for certificates whose subject starts with a wildcard character (*), see Accepted wildcards used by server certificates for server authentication. + + .PARAMETER FailoverPartner + The name of the failover partner server where database mirroring is configured. + + If the value of this key is "", then Initial Catalog must be present, and its value must not be "". + + The server name can be 128 characters or less. + + If you specify a failover partner but the failover partner server is not configured for database mirroring and the primary server (specified with the Server keyword) is not available, then the connection will fail. + + If you specify a failover partner and the primary server is not configured for database mirroring, the connection to the primary server (specified with the Server keyword) will succeed if the primary server is available. + + .PARAMETER IsActiveDirectoryUniversalAuth + Azure related + + .PARAMETER LockTimeout + Sets the time in seconds required for the connection to time out when the current transaction is locked. + + .PARAMETER MaxPoolSize + Sets the maximum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MinPoolSize + Sets the minimum number of connections allowed in the connection pool for this specific connection string. + + .PARAMETER MultipleActiveResultSets + When used, an application can maintain multiple active result sets (MARS). When false, an application must process or cancel all result sets from one batch before it can execute any other batch on that connection. + + .PARAMETER MultiSubnetFailover + If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting MultiSubnetFailover provides faster detection of and connection to the (currently) active server. For more information about SqlClient support for Always On Availability Groups + + .PARAMETER NetworkProtocol + Connect explicitly using 'TcpIp','NamedPipes','Multiprotocol','AppleTalk','BanyanVines','Via','SharedMemory' and 'NWLinkIpxSpx' + + .PARAMETER NonPooledConnection + Request a non-pooled connection + + .PARAMETER PacketSize + Sets the size in bytes of the network packets used to communicate with an instance of SQL Server. Must match at server. + + .PARAMETER PooledConnectionLifetime + When a connection is returned to the pool, its creation time is compared with the current time, and the connection is destroyed if that time span (in seconds) exceeds the value specified by Connection Lifetime. This is useful in clustered configurations to force load balancing between a running server and a server just brought online. + + A value of zero (0) causes pooled connections to have the maximum connection timeout. + + .PARAMETER SqlExecutionModes + The SqlExecutionModes enumeration contains values that are used to specify whether the commands sent to the referenced connection to the server are executed immediately or saved in a buffer. + + Valid values include CaptureSql, ExecuteAndCaptureSql and ExecuteSql. + + .PARAMETER StatementTimeout + Sets the number of seconds a statement is given to run before failing with a time-out error. + + .PARAMETER TrustServerCertificate + Sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust. + + .PARAMETER WorkstationId + Sets the name of the workstation connecting to SQL Server. + + .NOTES + Tags: Connection, Connect, ConnectionString + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSqlConnectionString + + .EXAMPLE + New-DbaSqlConnectionString -SqlInstance sql2014 + + Creates a connection string that connects using Windows Authentication + + .EXAMPLE + Connect-DbaInstance -SqlInstance sql2016 | New-DbaSqlConnectionString + + Builds a connected SMO object using Connect-DbaInstance then extracts and displays the connection string + + .EXAMPLE + $wincred = Get-Credential ad\sqladmin + New-DbaSqlConnectionString -SqlInstance sql2014 -Credential $wincred + + Creates a connection string that connects using alternative Windows credentials + + .EXAMPLE + $sqlcred = Get-Credential sqladmin + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -Credential $sqlcred + + Login to sql2014 as SQL login sqladmin. + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -ClientName "mah connection" + + Creates a connection string that connects using Windows Authentication and uses the client name "mah connection". So when you open up profiler or use extended events, you can search for "mah connection". + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -AppendConnectionString "Packet Size=4096;AttachDbFilename=C:\MyFolder\MyDataFile.mdf;User Instance=true;" + + Creates a connection string that connects to sql2014 using Windows Authentication, then it sets the packet size (this can also be done via -PacketSize) and other connection attributes. + + .EXAMPLE + $server = New-DbaSqlConnectionString -SqlInstance sql2014 -NetworkProtocol TcpIp -MultiSubnetFailover + + Creates a connection string with Windows Authentication that uses TCPIP and has MultiSubnetFailover enabled. + + .EXAMPLE + $connstring = New-DbaSqlConnectionString sql2016 -ApplicationIntent ReadOnly + + Creates a connection string with ReadOnly ApplicantionIntent. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("SqlCredential")] + [PSCredential]$Credential, + [string]$AccessToken, + [ValidateSet('ReadOnly', 'ReadWrite')] + [string]$ApplicationIntent, + [string]$BatchSeparator, + [string]$ClientName = "custom connection", + [int]$ConnectTimeout, + [switch]$EncryptConnection, + [string]$FailoverPartner, + [switch]$IsActiveDirectoryUniversalAuth, + [int]$LockTimeout, + [int]$MaxPoolSize, + [int]$MinPoolSize, + [switch]$MultipleActiveResultSets, + [switch]$MultiSubnetFailover, + [ValidateSet('TcpIp', 'NamedPipes', 'Multiprotocol', 'AppleTalk', 'BanyanVines', 'Via', 'SharedMemory', 'NWLinkIpxSpx')] + [string]$NetworkProtocol, + [switch]$NonPooledConnection, + [int]$PacketSize, + [int]$PooledConnectionLifetime, + [ValidateSet('CaptureSql', 'ExecuteAndCaptureSql', 'ExecuteSql')] + [string]$SqlExecutionModes, + [int]$StatementTimeout, + [switch]$TrustServerCertificate, + [string]$WorkstationId, + [string]$AppendConnectionString + ) + + process { + foreach ($instance in $sqlinstance) { + + if ($instance.GetType() -eq [Microsoft.SqlServer.Management.Smo.Server]) { + return $instance.ConnectionContext.ConnectionString + } + else { + $guid = [System.Guid]::NewGuid() + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $guid + + if ($AppendConnectionString) { + $connstring = $server.ConnectionContext.ConnectionString + $server.ConnectionContext.ConnectionString = "$connstring;$appendconnectionstring" + $server.ConnectionContext.ConnectionString + } + else { + + $server.ConnectionContext.ApplicationName = $clientname + + if ($AccessToken) { $server.ConnectionContext.AccessToken = $AccessToken } + if ($BatchSeparator) { $server.ConnectionContext.BatchSeparator = $BatchSeparator } + if ($ConnectTimeout) { $server.ConnectionContext.ConnectTimeout = $ConnectTimeout } + if ($Database) { $server.ConnectionContext.DatabaseName = $Database } + if ($EncryptConnection) { $server.ConnectionContext.EncryptConnection = $true } + if ($IsActiveDirectoryUniversalAuth) { $server.ConnectionContext.IsActiveDirectoryUniversalAuth = $true } + if ($LockTimeout) { $server.ConnectionContext.LockTimeout = $LockTimeout } + if ($MaxPoolSize) { $server.ConnectionContext.MaxPoolSize = $MaxPoolSize } + if ($MinPoolSize) { $server.ConnectionContext.MinPoolSize = $MinPoolSize } + if ($MultipleActiveResultSets) { $server.ConnectionContext.MultipleActiveResultSets = $true } + if ($NetworkProtocol) { $server.ConnectionContext.NetworkProtocol = $NetworkProtocol } + if ($NonPooledConnection) { $server.ConnectionContext.NonPooledConnection = $true } + if ($PacketSize) { $server.ConnectionContext.PacketSize = $PacketSize } + if ($PooledConnectionLifetime) { $server.ConnectionContext.PooledConnectionLifetime = $PooledConnectionLifetime } + if ($StatementTimeout) { $server.ConnectionContext.StatementTimeout = $StatementTimeout } + if ($SqlExecutionModes) { $server.ConnectionContext.SqlExecutionModes = $SqlExecutionModes } + if ($TrustServerCertificate) { $server.ConnectionContext.TrustServerCertificate = $true } + if ($WorkstationId) { $server.ConnectionContext.WorkstationId = $WorkstationId } + + $connstring = $server.ConnectionContext.ConnectionString + if ($MultiSubnetFailover) { $connstring = "$connstring;MultiSubnetFailover=True" } + if ($FailoverPartner) { $connstring = "$connstring;Failover Partner=$FailoverPartner" } + if ($ApplicationIntent) { $connstring = "$connstring;ApplicationIntent=$ApplicationIntent;" } + + if ($connstring -ne $server.ConnectionContext.ConnectionString) { + $server.ConnectionContext.ConnectionString = $connstring + } + if ($null -ne $Credential.username) { + $username = ($Credential.username).TrimStart("\") + + if ($username -like "*\*") { + $username = $username.Split("\")[1] + $authtype = "Windows Authentication with Credential" + $server.ConnectionContext.LoginSecure = $true + $server.ConnectionContext.ConnectAsUser = $true + $server.ConnectionContext.ConnectAsUserName = $username + $server.ConnectionContext.ConnectAsUserPassword = ($Credential).GetNetworkCredential().Password + } + else { + $authtype = "SQL Authentication" + $server.ConnectionContext.LoginSecure = $false + $server.ConnectionContext.set_Login($username) + $server.ConnectionContext.set_SecurePassword($Credential.Password) + } + } + + ($server.ConnectionContext.ConnectionString).Replace($guid, $SqlInstance) + } + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaSqlConnectionStringBuilder.ps1 b/functions/New-DbaSqlConnectionStringBuilder.ps1 new file mode 100644 index 000000000000..2c84f768024e --- /dev/null +++ b/functions/New-DbaSqlConnectionStringBuilder.ps1 @@ -0,0 +1,123 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function New-DbaSqlConnectionStringBuilder { + <# + .SYNOPSIS + Returns a System.Data.SqlClient.SqlConnectionStringBuilder with the string specified + + .DESCRIPTION + Creates a System.Data.SqlClient.SqlConnectionStringBuilder from a connection string. + + .PARAMETER ConnectionString + A Connection String + + .PARAMETER ApplicationName + The application name to tell SQL Server the connection is associated with. + + .PARAMETER DataSource + The Sql Server to connect to. + + .PARAMETER InitialCatalog + The initial database on the server to connect to. + + .PARAMETER IntegratedSecurity + Set to true to use windows authentication. + + .PARAMETER UserName + Sql User Name to connect with. + + .PARAMETER Password + Password to use to connect with. + + .PARAMETER MultipleActiveResultSets + Enable Multiple Active Result Sets. + + .PARAMETER ColumnEncryptionSetting + Enable Always Encrypted. + + .PARAMETER WorkstationID + Set the Workstation Id that is associated with the connection. + + .NOTES + Author: zippy1981 + Tags: SqlBuild, ConnectionString, Connection + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2017 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSqlConnectionStringBuilder + + .EXAMPLE + New-DbaSqlConnectionStringBuilder + + Returns an empty ConnectionStringBuilder + + .EXAMPLE + "Data Source=localhost,1433;Initial Catalog=AlwaysEncryptedSample;UID=sa;PWD=alwaysB3Encrypt1ng;Application Name=Always Encrypted Sample MVC App;Column Encryption Setting=enabled" | New-DbaSqlConnectionStringBuilder + + Returns a connection string builder that can be used to connect to the local sql server instance on the default port. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "")] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [string[]]$ConnectionString = "", + [Parameter(Mandatory = $false)] + [string]$ApplicationName = "dbatools Powershell Module", + [Parameter(Mandatory = $false)] + [string]$DataSource = $null, + [Parameter(Mandatory = $false)] + [string]$InitialCatalog = $null, + [Parameter(Mandatory = $false)] + [Nullable[bool]]$IntegratedSecurity = $null, + [Parameter(Mandatory = $false)] + [string]$UserName = $null, + # No point in securestring here, the memory is never stored securely in memory. + [Parameter(Mandatory = $false)] + [string]$Password = $null, + [Alias('MARS')] + [Parameter(Mandatory = $false)] + [switch]$MultipleActiveResultSets, + [Alias('AlwaysEncrypted')] + [Parameter(Mandatory = $false)] + [Data.SqlClient.SqlConnectionColumnEncryptionSetting]$ColumnEncryptionSetting = + [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled, + [Parameter(Mandatory = $false)] + [string]$WorkstationId = $env:COMPUTERNAME + ) + process { + foreach ($cs in $ConnectionString) { + $builder = New-Object Data.SqlClient.SqlConnectionStringBuilder $cs + if ($builder.ApplicationName -eq ".Net SqlClient Data Provider") { + $builder['Application Name'] = $ApplicationName + } + if (![string]::IsNullOrWhiteSpace($DataSource)) { + $builder['Data Source'] = $DataSource + } + if (![string]::IsNullOrWhiteSpace($InitialCatalog)) { + $builder['Initial Catalog'] = $InitialCatalog + } + if (![string]::IsNullOrWhiteSpace($IntegratedSecurity)) { + $builder['Integrated Security'] = $IntegratedSecurity + } + if (![string]::IsNullOrWhiteSpace($UserName)) { + $builder["User ID"] = $UserName + } + if (![string]::IsNullOrWhiteSpace($Password)) { + $builder['Password'] = $Password + } + if (![string]::IsNullOrWhiteSpace($WorkstationId)) { + $builder['Workstation ID'] = $WorkstationId + } + if ($MultipleActiveResultSets -eq $true) { + $builder['MultipleActiveResultSets'] = $true + } + if ($ColumnEncryptionSetting -eq [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled) { + $builder['Column Encryption Setting'] = [Data.SqlClient.SqlConnectionColumnEncryptionSetting]::Enabled + } + $builder + } + } +} \ No newline at end of file diff --git a/functions/New-DbaSqlDirectory.ps1 b/functions/New-DbaSqlDirectory.ps1 index 508d87f91971..7fee3299f416 100644 --- a/functions/New-DbaSqlDirectory.ps1 +++ b/functions/New-DbaSqlDirectory.ps1 @@ -1,102 +1,82 @@ -Function New-DbaSqlDirectory -{ -<# -.SYNOPSIS -Creates new path as specified by the path variable +function New-DbaSqlDirectory { + <# + .SYNOPSIS + Creates new path as specified by the path variable -.DESCRIPTION -Uses master.dbo.xp_create_subdir to create the path -Returns $true if the path can be created, $false otherwise + .DESCRIPTION + Uses master.dbo.xp_create_subdir to create the path + Returns $true if the path can be created, $false otherwise -.PARAMETER SqlServer -The SQL Server you want to run the test on. + .PARAMETER SqlInstance + The SQL Server you want to run the test on. -.PARAMETER Path -The Path to tests. Can be a file or directory. + .PARAMETER Path + The Path to tests. Can be a file or directory. -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. + .NOTES + Tags: Path, Directory, Folder + Author: Chrissy LeMaire (@cl), netnerds.net -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows -credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. + Requires: Admin access to server (not SQL Services), + Remoting must be enabled and accessible if $SqlInstance is not local + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT -.NOTES -Author: Chrissy LeMaire (@cl), netnerds.net -Requires: Admin access to server (not SQL Services), -Remoting must be enabled and accessible if $sqlserver is not local + .LINK + https://dbatools.io/New-DbaSqlDirectory -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire + .EXAMPLE + New-DbaSqlDirectory -SqlInstance sqlcluster -Path L:\MSAS12.MSSQLSERVER\OLAP -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + .EXAMPLE + $credential = Get-Credential + New-DbaSqlDirectory -SqlInstance sqlcluster -SqlCredential $credential -Path L:\MSAS12.MSSQLSERVER\OLAP -You should have received a copy of the GNU General Public License -along with this program. If not, see . + If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. Uses a SqlCredential to connect + #> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Parameter(Mandatory = $true)] + [string]$Path, + [PSCredential]$SqlCredential + ) + + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -.LINK -https://dbatools.io/New-DbaSqlDirectory + $Path = $Path.Replace("'", "''") -.EXAMPLE -New-DbaSqlDirectory -SqlServer sqlcluster -Path L:\MSAS12.MSSQLSERVER\OLAP + $exists = Test-DbaSqlPath -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Path $Path -If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. + if ($exists) { + Write-Warning "$Path already exists" + return + } -.EXAMPLE -$credential = Get-Credential -New-DbaSqlDirectory -SqlServer sqlcluster -SqlCredential $credential -Path L:\MSAS12.MSSQLSERVER\OLAP + $sql = "EXEC master.dbo.xp_create_subdir'$path'" + Write-Debug $sql -If the SQL Server instance sqlcluster can create the path L:\MSAS12.MSSQLSERVER\OLAP it will do and return $true, if not it will return $false. Uses a SqlCredential to connect -#> - [CmdletBinding()] - [OutputType([bool])] - param ( - [Parameter(Mandatory = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [Parameter(Mandatory = $true)] - [string]$Path, - [System.Management.Automation.PSCredential]$SqlCredential - ) - - $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential - - $Path = $Path.Replace("'", "''") - - $exists = Test-SqlPath -SqlServer $SqlServer -SqlCredential $SqlCredential -Path $Path - - if ($exists) - { - Write-Warning "$Path already exists" - return - } - - $sql = "EXEC master.dbo.xp_create_subdir'$path'" - Write-Debug $sql - - try - { - $query = $server.ConnectionContext.ExecuteNonQuery($sql) + try { + $query = $server.Query($sql) $Created = $true } - catch - { + catch { $Created = $false - } - - [pscustomobject]@{ - Server = $SqlServer - Path = $Path - Created = $Created - } + } + + [pscustomobject]@{ + Server = $SqlInstance + Path = $Path + Created = $Created + } } diff --git a/functions/New-DbaSsisCatalog.ps1 b/functions/New-DbaSsisCatalog.ps1 new file mode 100644 index 000000000000..c543209d9b8d --- /dev/null +++ b/functions/New-DbaSsisCatalog.ps1 @@ -0,0 +1,128 @@ +function New-DbaSsisCatalog { + <# + .SYNOPSIS + Enables the SSIS Catalog on a SQL Server 2012+ + + .DESCRIPTION + After installing the SQL Server Engine and SSIS you still have to enable the SSIS Catalog. This function will enable the catalog and gives the option of supplying the password. + + .PARAMETER SqlInstance + SQL Server you wish to run the function on. + + .PARAMETER SqlCredential + Credentials used to connect to the SQL Server + + .PARAMETER Password + Required password that will be used for the security key in SSISDB. + + .PARAMETER SsisCatalog + SSIS catalog name. By default, this is SSISDB. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: SSIS, SSISDB, Catalog + Author: Stephen Bennett, https://sqlnotesfromtheunderground.wordpress.com/ + Tags: + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaSsisCatalog + + .EXAMPLE + $password = ConvertTo-SecureString MyVisiblePassWord -AsPlainText -Force + New-DbaSsisCatalog -SqlInstance sql2016 -Password $password + + Creates the SSIS Catalog on server DEV01 with the specified password. + + .EXAMPLE + $password = Read-Host -AsSecureString -Prompt "Enter password" + New-DbaSsisCatalog -SqlInstance DEV01 -Password $password + + Creates the SSIS Catalog on server DEV01 with the specified password. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [Security.SecureString]$Password, + [string]$SsisCatalog = "SSISDB", + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + ## check if SSIS and Engine running on box + $services = Get-DbaSqlService -ComputerName $server.NetName + + $ssisservice = $Services | Where-Object { $_.ServiceType -eq "SSIS" -and $_.State -eq "Running" } + + if (-not $ssisservice) { + Stop-Function -Message "SSIS is not running on $instance" -Continue -Target $instance + } + + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + $clrenabled = Get-DbaSpConfigure -SqlInstance $server -Config IsSqlClrEnabled + + if (!$clrenabled.RunningValue) { + Stop-Function -Message 'CLR Integration must be enabled. You can enable it by running Set-DbaSpConfigure -SqlInstance sql2012 -Config IsSqlClrEnabled -Value $true' -Continue -Target $instance + } + + try { + $ssis = New-Object Microsoft.SqlServer.Management.IntegrationServices.IntegrationServices $server + } + catch { + Stop-Function -Message "Can't load server" -Target $instance -ErrorRecord $_ + return + } + + if ($ssis.Catalogs[$SsisCatalog]) { + Stop-Function -Message "SSIS Catalog already exists" -Continue -Target $ssis.Catalogs[$SsisCatalog] + } + else { + if ($Pscmdlet.ShouldProcess($server, "Creating SSIS catalog: $SsisCatalog")) { + try { + $ssisdb = New-Object Microsoft.SqlServer.Management.IntegrationServices.Catalog ($ssis, $SsisCatalog, $(([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))))) + $ssisdb.Create() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + SsisCatalog = $SsisCatalog + Created = $true + } + } + catch { + Stop-Function -Message "Failed to create SSIS Catalog: $_" -Target $_ -Continue + } + } + } + } + } +} diff --git a/functions/New-DbaXESession.ps1 b/functions/New-DbaXESession.ps1 new file mode 100644 index 000000000000..ad8a5299b9dc --- /dev/null +++ b/functions/New-DbaXESession.ps1 @@ -0,0 +1,70 @@ +function New-DbaXESession { + <# + .SYNOPSIS + Creates a new XESession object - for the dogged. + + .DESCRIPTION + Creates a new XESession object - for the dogged (very manual, Import-DbaXESession is recommended). See the following for more info: + + https://docs.microsoft.com/en-us/sql/relational-databases/extended-events/use-the-powershell-provider-for-extended-events + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The Name of the session to be created. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaXESession + + .EXAMPLE + $session = New-DbaXESession -SqlInstance sql2017 -Name XeSession_Test + $event = $session.AddEvent("sqlserver.file_written") + $event.AddAction("package0.callstack") + $session.Create() + + Returns a new XE Session object from sql2017 then adds an event, an action then creates it. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string]$Name, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $SqlConn = $server.ConnectionContext.SqlConnectionObject + $SqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $SqlConn + $store = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $SqlStoreConnection + + $store.CreateSession($Name) + } + } +} \ No newline at end of file diff --git a/functions/New-DbaXESmartCsvWriter.ps1 b/functions/New-DbaXESmartCsvWriter.ps1 new file mode 100644 index 000000000000..32569391b208 --- /dev/null +++ b/functions/New-DbaXESmartCsvWriter.ps1 @@ -0,0 +1,95 @@ +function New-DbaXESmartCsvWriter { + <# + .SYNOPSIS + This Response type is used to write Extended Events to a CSV file. + + .DESCRIPTION + This Response type is used to write Extended Events to a CSV file. + + .PARAMETER OutputFile + Specifies the path to the output CSV file. + + .PARAMETER Overwrite + Specifies whether any existiting file should be overwritten or not. + + .PARAMETER OutputColumn + Specifies the list of columns to output from the events. XESmartTarget will capture in memory and write to the target table only the columns (fields or targets) that are present in this list. + + Fields and actions are matched in a case-sensitive manner. + + Expression columns are supported. Specify a column with ColumnName AS Expression to add an expression column (Example: Total AS Reads + Writes) + + .PARAMETER Event + Specifies a list of events to be processed (with others being ignored. By default, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartCsvWriter + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $columns = "cpu_time", "duration", "physical_reads", "logical_reads", "writes", "row_count" + $response = New-DbaXESmartCsvWriter -OutputFile c:\temp\workload.csv -OutputColumn $columns -OverWrite -Event "sql_batch_completed" + Start-DbaXESmartTarget -SqlInstance localhost\sql2017 -Session "Profiler Standard" -Responder $response + + Writes Extended Events to the file "C:\temp\workload.csv". + #> + [CmdletBinding()] + param ( + [parameter(Mandatory)] + [string]$OutputFile, + [switch]$Overwrite, + [string[]]$Event, + [string[]]$OutputColumn, + [string]$Filter, + [switch]$EnableException + ) + + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + try { + $writer = New-Object -TypeName XESmartTarget.Core.Responses.CsvAppenderResponse + $writer.OutputFile = $OutputFile + $writer.OverWrite = $Overwrite + if (Test-Bound -ParameterName "Event") { + $writer.Events = $Event + } + if (Test-Bound -ParameterName "OutputColumn") { + $writer.OutputColumns = $OutputColumn + } + if (Test-Bound -ParameterName "Filter") { + $writer.Filter = $Filter + } + $writer + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } +} diff --git a/functions/New-DbaXESmartEmail.ps1 b/functions/New-DbaXESmartEmail.ps1 new file mode 100644 index 000000000000..16a49dc0d66d --- /dev/null +++ b/functions/New-DbaXESmartEmail.ps1 @@ -0,0 +1,147 @@ +function New-DbaXESmartEmail { + <# + .SYNOPSIS + This Response type can be used to send an email each time an event is captured. + + .DESCRIPTION + This Response type can be used to send an email each time an event is captured. + + .PARAMETER SmtpServer + Address of the SMTP server for outgoing mail. + + .PARAMETER Sender + Sender's email address. + + .PARAMETER To + Address of the To recipient(s). + + .PARAMETER Cc + Address of the Cc recipient(s). + + .PARAMETER Bcc + Address of the Bcc recipient(s). + + .PARAMETER Credential + Credential object containing username and password used to authenticate on the SMTP server. When blank, no authentication is performed. + + .PARAMETER Subject + Subject of the mail message. Accepts placeholders in the text. + + Placeholders are in the form {PropertyName}, where PropertyName is one of the fields or actions available in the Event object. + + For instance, a valid Subject in a configuration file looks like this: "An event of name {Name} occurred at {collection_time}" + + .PARAMETER Body + Body of the mail message. The body can be static text or any property taken from the underlying event. See Subject for a description of how placeholders work. + + .PARAMETER Attachment + Data to attach to the email message. At this time, it can be any of the fields/actions of the underlying event. The data from the field/action is attached to the message as an ASCII stream. A single attachment is supported. + + .PARAMETER AttachmentFileName + File name to assign to the attachment. + + .PARAMETER PlainText + If this switch is enabled, the email will be sent in plain text. By default, HTML formatting is used. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + You can specify a filter expression by using this attribute. The filter expression is in the same form that you would use in a SQL query. For example, a valid example looks like this: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartEmail + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $params = @{ + SmtpServer = "smtp.ad.local" + To = "admin@ad.local" + Sender = "reports@ad.local" + Subject = "Query executed" + Body = "Query executed at {collection_time}" + Attachment = "batch_text" + AttachmentFileName = "query.sql" + } + $emailresponse = New-DbaXESmartEmail @params + Start-DbaXESmartTarget -SqlInstance sql2017 -Session querytracker -Responder $emailresponse + + Sends an email each time a querytracker event is captured. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory)] + [string]$SmtpServer, + [parameter(Mandatory)] + [string]$Sender, + [parameter(Mandatory)] + [string[]]$To, + [string[]]$Cc, + [string[]]$Bcc, + [pscredential]$Credential, + [parameter(Mandatory)] + [string]$Subject, + [parameter(Mandatory)] + [string]$Body, + [string]$Attachment, + [string]$AttachmentFileName, + [string]$PlainText, + [string[]]$Event, + [string]$Filter, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll." -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + try { + $email = New-Object -TypeName XESmartTarget.Core.Responses.EmailResponse + $email.SmtpServer = $SmtpServer + $email.Sender = $Sender + $email.To = $To + $email.Cc = $Cc + $email.Bcc = $Bcc + $email.Subject = $Subject + $email.Body = $Body + $email.Attachment = $Attachment + $email.AttachmentFileName = $AttachmentFileName + $email.HTMLFormat = ($PlainText -eq $false) + if (Test-Bound -ParameterName "Event") { + $email.Events = $Event + } + if (Test-Bound -ParameterName "Filter") { + $email.Filter = $Filter + } + + if ($Credential) { + $email.UserName = $Credential.UserName + $email.Password = $Credential.GetNetworkCredential().Password + } + + $email + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } +} diff --git a/functions/New-DbaXESmartQueryExec.ps1 b/functions/New-DbaXESmartQueryExec.ps1 new file mode 100644 index 000000000000..ab934ffc7347 --- /dev/null +++ b/functions/New-DbaXESmartQueryExec.ps1 @@ -0,0 +1,105 @@ +function New-DbaXESmartQueryExec { + <# + .SYNOPSIS + This Response type executes a T-SQL command against a target database whenever an event is recorded. + + .DESCRIPTION + This Response type executes a T-SQL command against a target database whenever an event is recorded. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database that contains the target table. + + .PARAMETER Query + The T-SQL command to execute. This string can contain placeholders for properties taken from the events. + + Placeholders are in the form {PropertyName}, where PropertyName is one of the fields or actions available in the Event object. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + You can specify a filter expression by using this attribute. The filter expression is in the same form that you would use in a SQL query. For example, a valid example looks like this: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbaXESmartQueryExec + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $response = New-DbaXESmartQueryExec -SqlInstance sql2017 -Database dbadb -Query "update table set whatever = 1" + Start-DbaXESmartTarget -SqlInstance sql2017 -Session deadlock_tracker -Responder $response + + Executes a T-SQL command against dbadb on sql2017 whenever a deadlock event is recorded. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Database, + [string]$Query, + [switch]$EnableException, + [string[]]$Event, + [string]$Filter + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { + return + } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $execute = New-Object -TypeName XESmartTarget.Core.Responses.ExecuteTSQLResponse + $execute.ServerName = $server.Name + $execute.DatabaseName = $Database + $execute.TSQL = $Query + + if ($SqlCredential) { + $execute.UserName = $SqlCredential.UserName + $execute.Password = $SqlCredential.GetNetworkCredential().Password + } + + if (Test-Bound -ParameterName "Event") { + $execute.Events = $Event + } + if (Test-Bound -ParameterName "Filter") { + $execute.Filter = $Filter + } + + $execute + } + } +} diff --git a/functions/New-DbaXESmartReplay.ps1 b/functions/New-DbaXESmartReplay.ps1 new file mode 100644 index 000000000000..da5a5183c5cf --- /dev/null +++ b/functions/New-DbaXESmartReplay.ps1 @@ -0,0 +1,124 @@ +function New-DbaXESmartReplay { + <# + .SYNOPSIS + This Response type can be used to replay execution related events to a target SQL Server instance. + + .DESCRIPTION + This Response type can be used to replay execution related events to a target SQL Server instance. The events that you can replay are of the type sql_batch_completed and rpc_completed: all other events are ignored. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Name of the initial catalog to connect to. Statements will be replayed by changing database to the same database where the event was originally captured, so this property only controls the initial database to connect to. + + .PARAMETER Event + Each Response can be limited to processing specific events, while ignoring all the other ones. When this attribute is omitted, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER DelaySeconds + Specifies the duration of the delay in seconds. + + .PARAMETER ReplayIntervalSeconds + Specifies the duration of the replay interval in seconds. + + .PARAMETER StopOnError + If this switch is enabled, the replay will be stopped when the first error is encountered. By default, error messages are piped to the log and console output, and replay proceeds. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartReplay + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $response = New-DbaXESmartReplay -SqlInstance sql2017 -Database planning + Start-DbaXESmartTarget -SqlInstance sql2016 -Session loadrelay -Responder $response + + Replays events from sql2016 on sql2017 in the planning database. Returns a PowerShell job object. + + To see a list of all SmartTarget job objects, use Get-DbaXESmartTarget. + + .EXAMPLE + $response = New-DbaXESmartReplay -SqlInstance sql2017 -Database planning + Start-DbaXESmartTarget -SqlInstance sql2017 -Session 'Profiler Standard' -Responder $response -NotAsJob + + Replays events from the 'Profiler Standard' session on sql2016 to sql2017's planning database. Does not run as a job so you can see the raw output. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Database, + [string[]]$Event = "sql_batch_completed", + [string]$Filter, + [int]$DelaySeconds, + [switch]$StopOnError, + [int]$ReplayIntervalSeconds, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $replay = New-Object -TypeName XESmartTarget.Core.Responses.ReplayResponse + $replay.ServerName = $instance + $replay.DatabaseName = $Database + $replay.Events = $Event + $replay.StopOnError = $StopOnError + $replay.Filter = $Filter + $replay.DelaySeconds = $DelaySeconds + $replay.ReplayIntervalSeconds = $ReplayIntervalSeconds + + if ($SqlCredential) { + $replay.UserName = $SqlCredential.UserName + $replay.Password = $SqlCredential.GetNetworkCredential().Password + } + + $replay + } + catch { + $message = $_.Exception.InnerException.InnerException | Out-String + Stop-Function -Message $message -Target "XESmartTarget" -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/New-DbaXESmartTableWriter.ps1 b/functions/New-DbaXESmartTableWriter.ps1 new file mode 100644 index 000000000000..8297fa3583ec --- /dev/null +++ b/functions/New-DbaXESmartTableWriter.ps1 @@ -0,0 +1,132 @@ +function New-DbaXESmartTableWriter { + <# + .SYNOPSIS + This Response type is used to write Extended Events to a database table. + + .DESCRIPTION + This Response type is used to write Extended Events to a database table. The events are temporarily stored in memory before being written to the database at regular intervals. + + The target table can be created manually upfront or you can let the TableAppenderResponse create a target table based on the fields and actions available in the events captured. + + The columns of the target table and the fields/actions of the events are mapped by name (case-sensitive). + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the name of the database that contains the target table. + + .PARAMETER Table + Specifies the name of the target table. + + .PARAMETER AutoCreateTargetTable + If this switch is enabled, XESmartTarget will infer the definition of the target table from the columns captured in the Extended Events session. + + If the target table already exists, it will not be recreated. + + .PARAMETER UploadIntervalSeconds + Specifies the number of seconds XESmartTarget will keep the events in memory before dumping them to the target table. The default is 10 seconds. + + .PARAMETER OutputColumn + Specifies the list of columns to output from the events. XESmartTarget will capture in memory and write to the target table only the columns (fields or targets) that are present in this list. + + Fields and actions are matched in a case-sensitive manner. + + Expression columns are supported. Specify a column with ColumnName AS Expression to add an expression column (Example: Total AS Reads + Writes) + + .PARAMETER Event + Specifies a list of events to be processed (with others being ignored. By default, all events are processed. + + .PARAMETER Filter + Specifies a filter expression in the same form as you would use in the WHERE clause of a SQL query. + + Example: duration > 10000 AND cpu_time > 10000 + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/New-DbaXESmartTableWriter + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + $columns = "cpu_time", "duration", "physical_reads", "logical_reads", "writes", "row_count", "batch_text" + $response = New-DbaXESmartTableWriter -SqlInstance sql2017 -Database dbadb -Table deadlocktracker -OutputColumn $columns -Filter "duration > 10000" + Start-DbaXESmartTarget -SqlInstance sql2017 -Session deadlock_tracker -Responder $response + + Writes Extended Events to the deadlocktracker table in dbadb on sql2017. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [string]$Database, + [parameter(Mandatory)] + [string]$Table, + [switch]$AutoCreateTargetTable, + [int]$UploadIntervalSeconds = 10, + [string[]]$Event, + [string[]]$OutputColumn, + [string]$Filter, + [switch]$EnableException + ) + begin { + try { + Add-Type -Path "$script:PSModuleRoot\bin\XESmartTarget\XESmartTarget.Core.dll" -ErrorAction Stop + } + catch { + Stop-Function -Message "Could not load XESmartTarget.Core.dll" -ErrorRecord $_ -Target "XESmartTarget" + return + } + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 11 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + try { + $writer = New-Object -TypeName XESmartTarget.Core.Responses.TableAppenderResponse + $writer.ServerName = $server.Name + $writer.DatabaseName = $Database + $writer.TableName = $Table + $writer.AutoCreateTargetTable = $AutoCreateTargetTable + $writer.UploadIntervalSeconds = $UploadIntervalSeconds + if (Test-Bound -ParameterName "Event") { + $writer.Events = $Event + } + if (Test-Bound -ParameterName "OutputColumn") { + $writer.OutputColumns = $OutputColumn + } + if (Test-Bound -ParameterName "Filter") { + $writer.Filter = $Filter + } + $writer + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target "XESmartTarget" -Continue + } + } + } +} diff --git a/functions/New-DbatoolsSupportPackage.ps1 b/functions/New-DbatoolsSupportPackage.ps1 new file mode 100644 index 000000000000..bdc5d8e0ad47 --- /dev/null +++ b/functions/New-DbatoolsSupportPackage.ps1 @@ -0,0 +1,182 @@ +function New-DbatoolsSupportPackage { + <# + .SYNOPSIS + Creates a package of troubleshooting information that can be used by dbatools to help debug issues. + + .DESCRIPTION + This function creates an extensive debugging package that can help with reproducing and fixing issues. + + The file will be created on the desktop by default and will contain quite a bit of information: + - OS Information + - Hardware Information (CPU, Ram, things like that) + - .NET Information + - PowerShell Information + - Your input history + - The In-Memory message log + - The In-Memory error log + - Screenshot of the console buffer (Basically, everything written in your current console, even if you have to scroll upwards to see it. + + .PARAMETER Path + The folder where to place the output xml in. + + .PARAMETER Variables + Name of additional variables to attach. + This allows you to add the content of variables to the support package, if you believe them to be relevant to the case. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Fred Weinmann (@FredWeinmann) + Tags: Debug + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/New-DbatoolsSupportPackage + + .EXAMPLE + New-DbatoolsSupportPackage + + Creates a large support pack in order to help us troubleshoot stuff. + #> + [CmdletBinding()] + param ( + [string] + $Path = "$($env:USERPROFILE)\Desktop", + + [string[]] + $Variables, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + #region Helper functions + function Get-ShellBuffer { + [CmdletBinding()] + Param () + + try { + # Define limits + $rec = New-Object System.Management.Automation.Host.Rectangle + $rec.Left = 0 + $rec.Right = $host.ui.rawui.BufferSize.Width - 1 + $rec.Top = 0 + $rec.Bottom = $host.ui.rawui.BufferSize.Height - 1 + + # Load buffer + $buffer = $host.ui.rawui.GetBufferContents($rec) + + # Convert Buffer to list of strings + $int = 0 + $lines = @() + while ($int -le $rec.Bottom) { + $n = 0 + $line = "" + while ($n -le $rec.Right) { + $line += $buffer[$int, $n].Character + $n++ + } + $line = $line.TrimEnd() + $lines += $line + $int++ + } + + # Measure empty lines at the beginning + $int = 0 + $temp = $lines[$int] + while ($temp -eq "") { $int++; $temp = $lines[$int] } + + # Measure empty lines at the end + $z = $rec.Bottom + $temp = $lines[$z] + while ($temp -eq "") { $z--; $temp = $lines[$z] } + + # Skip the line launching this very function + $z-- + + # Measure empty lines at the end (continued) + $temp = $lines[$z] + while ($temp -eq "") { $z--; $temp = $lines[$z] } + + # Cut results to the limit and return them + return $lines[$int .. $z] + } + catch { } + } + #endregion Helper functions + } + PROCESS { + $filePathXml = "$($Path.Trim('\'))\dbatools_support_pack_$(Get-Date -Format "yyyy_MM_dd-HH_mm_ss").xml" + $filePathZip = $filePathXml -replace "\.xml$", ".zip" + + Write-Message -Level Critical -Message @" +Gathering information... +Will write the final output to: $filePathZip + +Please submit this file to the team, to help with troubleshooting whatever issue you encountered. +Be aware that this package contains a lot of information including your input history in the console. +Please make sure no sensitive data (such as passwords) can be caught this way. + +Ideally start a new console, perform the minimal steps required to reproduce the issue, then run this command. +This will make it easier for us to troubleshoot and you won't be sending us the keys to your castle. +"@ + + $hash = @{ } + Write-Message -Level Output -Message "Collecting dbatools logged messages (Get-DbatoolsLog)" + $hash["Messages"] = Get-DbatoolsLog + Write-Message -Level Output -Message "Collecting dbatools logged errors (Get-DbatoolsLog -Errors)" + $hash["Errors"] = Get-DbatoolsLog -Errors + Write-Message -Level Output -Message "Collecting copy of console buffer (what you can see on your console)" + $hash["ConsoleBuffer"] = Get-ShellBuffer + Write-Message -Level Output -Message "Collecting Operating System information (Win32_OperatingSystem)" + $hash["OperatingSystem"] = Get-DbaCmObject -ClassName Win32_OperatingSystem + Write-Message -Level Output -Message "Collecting CPU information (Win32_Processor)" + $hash["CPU"] = Get-DbaCmObject -ClassName Win32_Processor + Write-Message -Level Output -Message "Collecting Ram information (Win32_PhysicalMemory)" + $hash["Ram"] = Get-DbaCmObject -ClassName Win32_PhysicalMemory + Write-Message -Level Output -Message "Collecting PowerShell & .NET Version (`$PSVersionTable)" + $hash["PSVersion"] = $PSVersionTable + Write-Message -Level Output -Message "Collecting Input history (Get-History)" + $hash["History"] = Get-History + Write-Message -Level Output -Message "Collecting list of loaded modules (Get-Module)" + $hash["Modules"] = Get-Module + Write-Message -Level Output -Message "Collecting list of loaded snapins (Get-PSSnapin)" + $hash["SnapIns"] = Get-PSSnapin + Write-Message -Level Output -Message "Collecting list of loaded assemblies (Name, Version, and Location)" + $hash["Assemblies"] = [appdomain]::CurrentDomain.GetAssemblies() | Select-Object CodeBase, FullName, Location, ImageRuntimeVersion, GlobalAssemblyCache, IsDynamic + + if (Test-Bound "Variables") { + Write-Message -Level Output -Message "Adding variables specified for export: $($Variables -join ", ")" + $hash["Variables"] = $Variables | Get-Variable -ErrorAction Ignore + } + + $data = [pscustomobject]$hash + + try { $data | Export-Clixml -Path $filePathXml -ErrorAction Stop } + catch { + Stop-Function -Message "Failed to export dump to file!" -ErrorRecord $_ -Target $filePathXml + return + } + + try { Compress-Archive -Path $filePathXml -DestinationPath $filePathZip -ErrorAction Stop } + catch { + Stop-Function -Message "Failed to pack dump-file into a zip archive. Please do so manually before submitting the results as the unpacked xml file will be rather large." -ErrorRecord $_ -Target $filePathZip + return + } + + Remove-Item -Path $filePathXml -ErrorAction Ignore + } + END { + Write-Message -Level InternalComment -Message "Ending" + } +} \ No newline at end of file diff --git a/functions/Out-DbaDataTable.ps1 b/functions/Out-DbaDataTable.ps1 deleted file mode 100644 index 0dee7ed2bd8c..000000000000 --- a/functions/Out-DbaDataTable.ps1 +++ /dev/null @@ -1,127 +0,0 @@ -Function Out-DbaDataTable -{ -<# -.SYNOPSIS -Creates a DataTable for an object - -.DESCRIPTION -Creates a DataTable based on an objects properties. This allows you to easily write to SQL Server tables - -Thanks to Chad Miller, this script is all him. https://gallery.technet.microsoft.com/scriptcenter/4208a159-a52e-4b99-83d4-8048468d29dd - -.PARAMETER InputObject -The object to transform into a DataTable - -.NOTES -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK - https://dbatools.io/Out-DbaDataTable - -.EXAMPLE -Get-Service | Out-DbaDataTable - -Creates a $datatable based off of the output of Get-Service - -.EXAMPLE -Out-DbaDataTable -InputObject $csv.cheesetypes - -Creates a DataTable from the CSV object, $csv.cheesetypes - -.EXAMPLE -$dblist | Out-DbaDataTable - -Similar to above but $dbalist gets piped in - -#> - [CmdletBinding()] - param ( - [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [PSObject[]]$InputObject - ) - - BEGIN - { - function Get-Type - { - param ($type) - - $types = @( - 'System.Boolean', - 'System.Byte[]', - 'System.Byte', - 'System.Char', - 'System.Datetime', - 'System.Decimal', - 'System.Double', - 'System.Guid', - 'System.Int16', - 'System.Int32', - 'System.Int64', - 'System.Single', - 'System.UInt16', - 'System.UInt32', - 'System.UInt64') - - if ($types -contains $type) - { - return $type - } - else - { - return 'System.String' - } - } - - $datatable = New-Object System.Data.DataTable - } - - PROCESS - - { - foreach ($object in $InputObject) - { - $datarow = $datatable.NewRow() - foreach ($property in $object.PsObject.get_properties()) - { - if ($datatable.Rows.Count -eq 0) - { - $column = New-Object System.Data.DataColumn - $column.ColumnName = $property.Name.ToString() - - if ($property.value) - { - if ($property.value -isnot [System.DBNull]) - { - $type = Get-Type $property.TypeNameOfValue - $column.DataType = [System.Type]::GetType($type) - } - } - $datatable.Columns.Add($column) - } - if ($property.Gettype().IsArray) - { - $datarow.Item($property.Name) = $property.value | ConvertTo-XML -AS String -NoTypeInformation -Depth 1 - } - else - { - if($property.value.length -gt 0) - { - $datarow.Item($property.Name) = $property.value - } - } - } - $datatable.Rows.Add($datarow) - } - } - - End - { - return @( ,($datatable)) - } - -} diff --git a/functions/Publish-DbaDacpac.ps1 b/functions/Publish-DbaDacpac.ps1 new file mode 100644 index 000000000000..085a873db004 --- /dev/null +++ b/functions/Publish-DbaDacpac.ps1 @@ -0,0 +1,267 @@ +function Publish-DbaDacpac { + <# + .SYNOPSIS + The Publish-Database command takes a dacpac which is the output from an SSDT project and publishes it to a database. Changing the schema to match the dacpac and also to run any scripts in the dacpac (pre/post deploy scripts). + + .DESCRIPTION + Deploying a dacpac uses the DacFx which historically needed to be installed on a machine prior to use. In 2016 the DacFx was supplied by Microsoft as a nuget package and this uses that nuget package. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the filesystem path to the DACPAC + + .PARAMETER PublishXml + Specifies the publish profile which will include options and sqlCmdVariables. + + .PARAMETER Database + Specifies the name of the database being published. + + .PARAMETER ConnectionString + Specifies the connection string to the database you are upgrading. This is not required if SqlInstance is specified. + + .PARAMETER GenerateDeploymentScript + If this switch is enabled, the publish script will be generated. + + .PARAMETER GenerateDeploymentReport + If this switch is enabled, the publish XML report will be generated. + + .PARAMETER OutputPath + Specifies the filesystem path (directory) where output files will be generated. + + .PARAMETER ScriptOnly + If this switch is enabled, only the change scripts will be generated. + + .PARAMETER IncludeSqlCmdVars + If this switch is enabled, SqlCmdVars in publish.xml will have their values overwritten. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER DacFxPath + Path to the dac dll. If this is ommited, then the version of dac dll which is packaged with dbatools is used. + + .NOTES + Tags: Migration, Database, Dacpac + Author: Richie lee (@bzzzt_io) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Publish-DbaDacpac + + .EXAMPLE + Publish-DbaDacpac -SqlInstance sql2017 -Database WideWorldImporters -Path C:\temp\sql2016-WideWorldImporters.dacpac -PublishXml C:\temp\sql2016-WideWorldImporters-publish.xml + + Updates WideWorldImporters on sql2017 from the sql2016-WideWorldImporters.dacpac using the sql2016-WideWorldImporters-publish.xml publish profile + + .EXAMPLE + New-DbaPublishProfile -SqlInstance sql2016 -Database db2 -Path C:\temp + Export-DbaDacpac -SqlInstance sql2016 -Database db2 | Publish-DbaDacpac -PublishXml C:\temp\sql2016-db2-publish.xml -Database db1, db2 -SqlInstance sql2017 + + Creates a publish profile at C:\temp\sql2016-db2-publish.xml, exports the .dacpac to $home\Documents\sql2016-db2.dacpac + then publishes it to the sql2017 server database db2 + + .EXAMPLE + $loc = "C:\Users\bob\source\repos\Microsoft.Data.Tools.Msbuild\lib\net46\Microsoft.SqlServer.Dac.dll" + Publish-DbaDacpac -SqlInstance "local" -Database WideWorldImporters -Path C:\temp\WideWorldImporters.dacpac -PublishXml C:\temp\WideWorldImporters.publish.xml -DacFxPath $loc + #> + [CmdletBinding()] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$Path, + [Parameter(Mandatory)] + [string]$PublishXml, + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string[]]$Database, + [string[]]$ConnectionString, + [switch]$GenerateDeploymentScript, + [switch]$GenerateDeploymentReport, + [Switch]$ScriptOnly, + [string]$OutputPath = "$home\Documents", + [switch]$IncludeSqlCmdVars, + [switch]$EnableException, + [String]$DacFxPath + ) + + begin { + if ((Test-Bound -Not -ParameterName SqlInstance) -and (Test-Bound -Not -ParameterName ConnectionString)) { + Stop-Function -Message "You must specify either SqlInstance or ConnectionString." + } + if ((Test-Bound -ParameterName GenerateDeploymentScript) -or (Test-Bound -ParameterName GenerateDeploymentReport)) { + $defaultcolumns = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Dacpac', 'PublishXml', 'Result', 'DatabaseScriptPath', 'MasterDbScriptPath', 'DeploymentReport', 'DeployOptions', 'SqlCmdVariableValues' + } + else { + $defaultcolumns = 'ComputerName', 'InstanceName', 'SqlInstance', 'Database', 'Dacpac', 'PublishXml', 'Result', 'DeployOptions', 'SqlCmdVariableValues' + } + if ((Test-Bound -ParameterName ScriptOnly) -and (Test-Bound -Not -ParameterName GenerateDeploymentScript) -and (Test-Bound -Not -ParameterName GenerateDeploymentScript)) { + Stop-Function -Message "You must at least one of GenerateDeploymentScript or GenerateDeploymentReport when using ScriptOnly" + } + + function Get-ServerName ($connstring) { + $builder = New-Object System.Data.Common.DbConnectionStringBuilder + $builder.set_ConnectionString($connstring) + $instance = $builder['data source'] + + if (-not $instance) { + $instance = $builder['server'] + } + + return $instance.ToString().Replace('\', '-').Replace('(','').Replace(')','') + } + if (Test-Bound -Not -ParameterName 'DacfxPath'){ + $dacfxPath = "$script:PSModuleRoot\bin\smo\Microsoft.SqlServer.Dac.dll" + } + + if ((Test-Path $dacfxPath) -eq $false) { + Stop-Function -Message 'No usable version of Dac Fx found.' -EnableException $EnableException + } + else { + try { + Add-Type -Path $dacfxPath + Write-Message -Level Verbose -Message "Dac Fx loaded." + } + catch { + Stop-Function -Message 'No usable version of Dac Fx found.' -EnableException $EnableException -ErrorRecord $_ + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + if (-not (Test-Path -Path $Path)) { + Stop-Function -Message "$Path not found!" + } + + if (-not (Test-Path -Path $PublishXml)) { + Stop-Function -Message "$PublishXml not found!" + } + + foreach ($instance in $sqlinstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $ConnectionString += $server.ConnectionContext.ConnectionString.Replace('"', "'") + } + + try { + $dacPackage = [Microsoft.SqlServer.Dac.DacPackage]::Load($Path) + } + catch { + Stop-Function -Message "Could not load package." -ErrorRecord $_ + } + + try { + $dacProfile = [Microsoft.SqlServer.Dac.DacProfile]::Load($PublishXml) + } + catch { + Stop-Function -Message "Could not load profile." -ErrorRecord $_ + } + + if ($IncludeSqlCmdVars) { + Get-SqlCmdVars -SqlCommandVariableValues $dacProfile.DeployOptions.SqlCommandVariableValues + } + + foreach ($connstring in $ConnectionString) { + $cleaninstance = Get-ServerName $connstring + $instance = $cleaninstance.ToString().Replace('--', '\') + + foreach ($dbname in $database) { + if ($GenerateDeploymentScript -or $GenerateDeploymentReport) { + $timeStamp = (Get-Date).ToString("yyMMdd_HHmmss_f") + $DatabaseScriptPath = Join-Path $OutputPath "$cleaninstance-$dbname`_DeployScript_$timeStamp.sql" + $MasterDbScriptPath = Join-Path $OutputPath "$cleaninstance-$dbname`_Master.DeployScript_$timeStamp.sql" + $DeploymentReport = Join-Path $OutputPath "$cleaninstance-$dbname`_Result.DeploymentReport_$timeStamp.xml" + } + + if ($connstring -notmatch 'Database=') { + $connstring = "$connstring;Database=$dbname" + } + + try { + $dacServices = New-Object Microsoft.SqlServer.Dac.DacServices $connstring + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server -Continue + } + + $options = @{ + GenerateDeploymentScript = $GenerateDeploymentScript + GenerateDeploymentReport = $GenerateDeploymentReport + DatabaseScriptPath = $DatabaseScriptPath + MasterDbScriptPath = $MasterDbScriptPath + DeployOptions = $dacProfile.DeployOptions + } + + try { + $global:output = @() + Register-ObjectEvent -InputObject $dacServices -EventName "Message" -SourceIdentifier "msg" -Action { $global:output += $EventArgs.Message.Message } | Out-Null + if ($ScriptOnly) { + Write-Message -Level Verbose -Message "Generating script." + $result = $dacServices.Script($dacPackage, $dbname, $options) + } + else { + Write-Message -Level Verbose -Message "Executing Deployment." + $result = $dacServices.Publish($dacPackage, $dbname, $options) + } + } + catch [Microsoft.SqlServer.Dac.DacServicesException] { + Stop-Function -Message "Deployment failed" -ErrorRecord $_ -EnableException $true + } + finally { + Unregister-Event -SourceIdentifier "msg" + if ($GenerateDeploymentReport) { + $result.DeploymentReport | Out-File $DeploymentReport + Write-Message -Level Verbose -Message "Deployment Report - $DeploymentReport." + } + if ($GenerateDeploymentScript) { + Write-Message -Level Verbose -Message "Database change script - $DatabaseScriptPath." + if ((Test-Path $MasterDbScriptPath)) { + Write-Message -Level Verbose -Message "Master database change script - $($result.MasterDbScript)." + } + } + $resultoutput = ($global:output -join "`r`n" | Out-String).Trim() + if ($resultoutput -match "Failed" -and ($GenerateDeploymentReport -or $GenerateDeploymentScript)) { + Write-Message -Level Warning -Message "Seems like the attempt to publish/script may have failed. If scripts have not generated load dacpac into Visual Studio to check SQL is valid." + } + $server = [dbainstance]$instance + $deployOptions = $dacProfile.DeployOptions | Select-Object -Property * -ExcludeProperty "SqlCommandVariableValues" + [pscustomobject]@{ + ComputerName = $server.ComputerName + InstanceName = $server.InstanceName + SqlInstance = $server.FullName + Database = $dbname + Result = $resultoutput + Dacpac = $Path + PublishXml = $PublishXml + ConnectionString = $connstring + DatabaseScriptPath = $DatabaseScriptPath + MasterDbScriptPath = $MasterDbScriptPath + DeploymentReport = $DeploymentReport + DeployOptions = $deployOptions + SqlCmdVariableValues = $dacProfile.DeployOptions.SqlCommandVariableValues.Keys + + } | Select-DefaultView -Property $defaultcolumns + } + } + } + } +} \ No newline at end of file diff --git a/functions/Read-DbaAuditFile.ps1 b/functions/Read-DbaAuditFile.ps1 new file mode 100644 index 000000000000..2fe0553fa5fc --- /dev/null +++ b/functions/Read-DbaAuditFile.ps1 @@ -0,0 +1,143 @@ +function Read-DbaAuditFile { + <# + .SYNOPSIS + Read Audit details from a sqlaudit file. + + .DESCRIPTION + Read Audit details from a sqlaudit file. + + .PARAMETER Path + The path to the sqlaudit file. This is relative to the computer executing the command. UNC paths are supported. + + .PARAMETER Exact + If this switch is enabled, only an exact search will be used for the Path. By default, this command will add a wildcard to the Path because Eventing uses the file name as a template and adds characters. + + .PARAMETER Raw + If this switch is enabled, the Microsoft.SqlServer.XEvent.Linq.PublishedEvent enumeration object will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, Audit + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaAuditFile + + .EXAMPLE + Read-DbaAuditFile -Path C:\temp\logins.sqlaudit + + Returns events from C:\temp\logins.sqlaudit. + + .EXAMPLE + Get-ChildItem C:\temp\audit\*.sqlaudit | Read-DbaAuditFile + + Returns events from all .sqlaudit files in C:\temp\audit. + + .EXAMPLE + Get-DbaServerAudit -SqlInstance sql2014 -Audit LoginTracker | Read-DbaAuditFile + + Reads remote Audit details by accessing the file over the admin UNC share. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('FullName')] + [object[]]$Path, + [switch]$Exact, + [switch]$Raw, + [switch]$EnableException + ) + process { + foreach ($file in $path) { + # in order to ensure CSV gets all fields, all columns will be + # collected and output in the first (all all subsequent) object + $columns = @("name", "timestamp") + + if ($file -is [System.String]) { + $currentfile = $file + $manualadd = $true + } + elseif ($file -is [System.IO.FileInfo]) { + $currentfile = $file.FullName + $manualadd = $true + } + else { + if ($file -isnot [Microsoft.SqlServer.Management.Smo.Audit]) { + Stop-Function -Message "Unsupported file type." + return + } + + if ($file.FullName.Length -eq 0) { + Stop-Function -Message "This Audit does not have an associated file." + return + } + + $instance = [dbainstance]$file.ComputerName + + if ($instance.IsLocalHost) { + $currentfile = $file.FullName + } + else { + $currentfile = $file.RemoteFullName + } + } + + if (-not $Exact) { + $currentfile = $currentfile.Replace('.sqlaudit', '*.sqlaudit') + + if ($currentfile -notmatch "sqlaudit") { + $currentfile = "$currentfile*.sqlaudit" + } + } + + $accessible = Test-Path -Path $currentfile + $whoami = whoami + + if (-not $accessible) { + if ($file.Status -eq "Stopped") { continue } + Stop-Function -Continue -Message "$currentfile cannot be accessed from $($env:COMPUTERNAME). Does $whoami have access?" + } + + if ($raw) { + return New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + } + + $enum = New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + $newcolumns = ($enum.Fields.Name | Select-Object -Unique) + + $actions = ($enum.Actions.Name | Select-Object -Unique) + foreach ($action in $actions) { + $newcolumns += ($action -Split '\.')[-1] + } + + $newcolumns = $newcolumns | Sort-Object + $columns = ($columns += $newcolumns) | Select-Object -Unique + + # Make it selectable, otherwise it's a weird enumeration + foreach ($event in (New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile))) { + $hash = [ordered]@{ } + + foreach ($column in $columns) { + $null = $hash.Add($column, $event.$column) + } + + foreach ($action in $event.Actions) { + $hash[$action.Name] = $action.Value + } + + foreach ($field in $event.Fields) { + $hash[$field.Name] = $field.Value + } + + [pscustomobject]$hash + } + } + } +} \ No newline at end of file diff --git a/functions/Read-DbaBackupHeader.ps1 b/functions/Read-DbaBackupHeader.ps1 index e99c248daf07..5ae74e5d8fd9 100644 --- a/functions/Read-DbaBackupHeader.ps1 +++ b/functions/Read-DbaBackupHeader.ps1 @@ -1,237 +1,288 @@ -Function Read-DbaBackupHeader -{ -<# -.SYNOPSIS -Reads and displays detailed information about a SQL Server backup - -.DESCRIPTION -Reads full, differential and transaction log backups. An online SQL Server is required to parse the backup files and the path specified must be relative to that SQL Server. - -.PARAMETER SqlServer -The SQL Server instance. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. - -.PARAMETER Path -Path to SQL Server backup file. This can be a full, differential or log backup file. - -.PARAMETER Simple -Returns fewer columns for an easy overview - -.PARAMETER FileList -Returns detailed information about the files within the backup - -.NOTES -Tags: DisasterRecovery, Backup, Restore -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Read-DbaBackupHeader - -.EXAMPLE -Read-DbaBackupHeader -SqlServer sql2016 -Path S:\backups\mydb\mydb.bak - -Logs into sql2016 using Windows authentication and reads the local file on sql2016, S:\backups\mydb\mydb.bak. - -If you are running this command on a workstation and connecting remotely, remember that sql2016 cannot access files on your own workstation. - -.EXAMPLE -Read-DbaBackupHeader -SqlServer sql2016 -Path \\nas\sql\backups\mydb\mydb.bak, \\nas\sql\backups\otherdb\otherdb.bak - -Logs into sql2016 and reads two backup files - mydb.bak and otherdb.bak. The SQL Server service account must have rights to read this file. - -.EXAMPLE -Read-DbaBackupHeader -SqlServer . -Path C:\temp\myfile.bak -Simple - -Logs into the local workstation (or computer) and shows simplified output about C:\temp\myfile.bak. The SQL Server service account must have rights to read this file. - -.EXAMPLE -$backupinfo = Read-DbaBackupHeader -SqlServer . -Path C:\temp\myfile.bak -$backupinfo.FileList - -Displays detailed information about each of the datafiles contained in the backupset. - -.EXAMPLE -Read-DbaBackupHeader -SqlServer . -Path C:\temp\myfile.bak -FileList - -Also returns detailed information about each of the datafiles contained in the backupset. - -.EXAMPLE -"C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" | Read-DbaBackupHeader -SqlServer sql2016 - -Similar to running Read-DbaBackupHeader -SqlServer sql2016 -Path "C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" - -.EXAMPLE -Get-ChildItem \\nas\sql\*.bak | Read-DbaBackupHeader -SqlServer sql2016 - -Gets a list of all .bak files on the \\nas\sql share and reads the headers using the server named "sql2016". This means that the server, sql2016, must have read access to the \\nas\sql share. -#> - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [object]$SqlCredential, - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [object[]]$Path, - [switch]$Simple, - [switch]$FileList - ) - - BEGIN - { - $LoopCnt = 1 - $FunctionName = $FunctionName = (Get-PSCallstack)[0].Command - Write-Verbose "$FunctionName - Connecting to $SqlServer" - try - { - $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $Credential -ErrorVariable ConnectError - - } - catch - { - Write-Warning $_ - continue - } - } - - PROCESS - { - - $PathCount = $path.length - Write-verbose "$FunctionName - $pathcount files to scan" - foreach ($file in $path) - { - if ($file.FullName -ne $null) { $file = $file.FullName } - Write-Progress -Id 1 -Activity Updating -Status 'Progress' -CurrentOperation "Scanning Restore headers on File $LoopCnt - $file" - - Write-Verbose "$FunctionName - Scanning file $file" - $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore - $device = New-Object Microsoft.SqlServer.Management.Smo.BackupDeviceItem $file, FILE - $restore.Devices.Add($device) - if (Test-SqlPath -SqlServer $server -Path $file) - { - <# try - { - $allfiles = $restore.ReadFileList($server) - } - catch - { - $shortname = Split-Path $file -Leaf - if (!(Test-SqlPath -SqlServer $server -Path $file)) - { - Write-Warning "File $shortname does not exist or access denied. The SQL Server service account may not have access to the source directory." - } - else - { - Write-Warning "File list for $shortname could not be determined. This is likely due to the file not existing, the backup version being incompatible or unsupported, connectivity issues or tiemouts with the SQL Server, or the SQL Server service account does not have access to the source directory." - } - } - - - }#> - try - { - $datatable = $restore.ReadBackupHeader($server) - } - catch - { - Write-Warning "$FunctionName - Problem with $file" - Return - } - $fl = $datatable.Columns.Add("FileList", [object]) - #$datatable.rows[0].FileList = $allfiles.rows - - $mb = $datatable.Columns.Add("BackupSizeMB", [int]) - $mb.Expression = "BackupSize / 1024 / 1024" - $gb = $datatable.Columns.Add("BackupSizeGB") - $gb.Expression = "BackupSizeMB / 1024" - - if ($null -eq $datatable.Columns['CompressedBackupSize']) - { - $formula = "0" - } - else - { - $formula = "CompressedBackupSize / 1024 / 1024" - } - - $cmb = $datatable.Columns.Add("CompressedBackupSizeMB", [int]) - $cmb.Expression = $formula - $cgb = $datatable.Columns.Add("CompressedBackupSizeGB") - $cgb.Expression = "CompressedBackupSizeMB / 1024" - - $null = $datatable.Columns.Add("SqlVersion") - - - $null = $datatable.Columns.Add("BackupPath") - # $datatable.Columns["BackupPath"].DefaultValue = $Path - $dbversion = $datatable.Rows[0].DatabaseVersion - - # $datatable.Rows[0].SqlVersion = (Convert-DbVersionToSqlVersion $dbversion) - $BackupSlot = 1 - ForEach ($row in $DataTable) - { - $row.SqlVersion = (Convert-DbVersionToSqlVersion $dbversion) - $row.BackupPath = $file - try - { - $restore.FileNumber = $BackupSlot - $allfiles = $restore.ReadFileList($server) - } - catch - { - $shortname = Split-Path $file -Leaf - if (!(Test-SqlPath -SqlServer $server -Path $file)) - { - Write-Warning "File $shortname does not exist or access denied. The SQL Server service account may not have access to the source directory." - } - else - { - Write-Warning "File list for $shortname could not be determined. This is likely due to the file not existing, the backup version being incompatible or unsupported, connectivity issues or tiemouts with the SQL Server, or the SQL Server service account does not have access to the source directory." - } - - #Write-Exception $_ - #return - } - $row.FileList = $allfiles - $BackupSlot++ - - } - } - else - { - Write-Warning "File $shortname does not exist or access denied. The SQL Server service account may not have access to the source directory." - return - } - if ($Simple) - { - $datatable | Select-Object DatabaseName, BackupFinishDate, RecoveryModel, BackupSizeMB, CompressedBackupSizeMB, DatabaseCreationDate, UserName, ServerName, SqlVersion, BackupPath - } - elseif ($filelist) - { - $datatable.filelist - } - else - { - $datatable - } - - Remove-Variable DataTable -ErrorAction SilentlyContinue - } - $LoopCnt++ - } - - END - { - $server.ConnectionContext.Disconnect() - } +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Read-DbaBackupHeader { + <# + .SYNOPSIS + Reads and displays detailed information about a SQL Server backup. + + .DESCRIPTION + Reads full, differential and transaction log backups. An online SQL Server is required to parse the backup files and the path specified must be relative to that SQL Server. + + .PARAMETER SqlInstance + The SQL Server instance to use for parsing the backup files. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Path to SQL Server backup file. This can be a full, differential or log backup file. Accepts valid filesystem paths and URLs. + + .PARAMETER Simple + If this switch is enabled, fewer columns are returned, giving an easy overview. + + .PARAMETER FileList + If this switch is enabled, detailed information about the files within the backup is returned. + + .PARAMETER AzureCredential + Name of the SQL Server credential that should be used for Azure storage access. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaBackupHeader + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance sql2016 -Path S:\backups\mydb\mydb.bak + + Logs into sql2016 using Windows authentication and reads the local file on sql2016, S:\backups\mydb\mydb.bak. + + If you are running this command on a workstation and connecting remotely, remember that sql2016 cannot access files on your own workstation. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance sql2016 -Path \\nas\sql\backups\mydb\mydb.bak, \\nas\sql\backups\otherdb\otherdb.bak + + Logs into sql2016 and reads two backup files - mydb.bak and otherdb.bak. The SQL Server service account must have rights to read this file. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak -Simple + + Logs into the local workstation (or computer) and shows simplified output about C:\temp\myfile.bak. The SQL Server service account must have rights to read this file. + + .EXAMPLE + $backupinfo = Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak + $backupinfo.FileList + + Displays detailed information about each of the datafiles contained in the backupset. + + .EXAMPLE + Read-DbaBackupHeader -SqlInstance . -Path C:\temp\myfile.bak -FileList + + Also returns detailed information about each of the datafiles contained in the backupset. + + .EXAMPLE + "C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" | Read-DbaBackupHeader -SqlInstance sql2016 + + Similar to running Read-DbaBackupHeader -SqlInstance sql2016 -Path "C:\temp\myfile.bak", "\backupserver\backups\myotherfile.bak" + + .EXAMPLE + Get-ChildItem \\nas\sql\*.bak | Read-DbaBackupHeader -SqlInstance sql2016 + + Gets a list of all .bak files on the \\nas\sql share and reads the headers using the server named "sql2016". This means that the server, sql2016, must have read access to the \\nas\sql share. + + .EXAMPLE + Read-DbaBackupHeader -Path https://dbatoolsaz.blob.core.windows.net/azbackups/restoretime/restoretime_201705131850.bak + -AzureCredential AzureBackupUser + + Gets the backup header information from the SQL Server backup file stored at https://dbatoolsaz.blob.core.windows.net/azbackups/restoretime/restoretime_201705131850.bak on Azure + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", '')] + <# AzureCredential is utilized in this command is not a formal Credential object. #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance]$SqlInstance, + [PsCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Path, + [switch]$Simple, + [switch]$FileList, + [string]$AzureCredential, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + foreach($p in $path) { + if ([System.IO.Path]::GetExtension($p).Length -eq 0) { + Stop-Function -Message "Path ($p) should be a file, not a folder" -Category InvalidArgument + return + } + } + Write-Message -Level InternalComment -Message "Starting reading headers" + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + $getHeaderScript = { + Param ( + $SqlInstance, + $Path, + $DeviceType, + $AzureCredential + ) + #Copy existing connection to create an independent TSQL session + $server = New-Object Microsoft.SqlServer.Management.Smo.Server $SqlInstance.ConnectionContext.Copy() + $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + + if ($DeviceType -eq 'URL') { + $restore.CredentialName = $AzureCredential + } + + $device = New-Object Microsoft.SqlServer.Management.Smo.BackupDeviceItem $Path, $DeviceType + $restore.Devices.Add($device) + $dataTable = $restore.ReadBackupHeader($server) + + $null = $dataTable.Columns.Add("FileList", [object]) + + $mb = $dataTable.Columns.Add("BackupSizeMB", [int]) + $mb.Expression = "BackupSize / 1024 / 1024" + $gb = $dataTable.Columns.Add("BackupSizeGB") + $gb.Expression = "BackupSizeMB / 1024" + + if ($null -eq $dataTable.Columns['CompressedBackupSize']) { + $formula = "0" + } + else { + $formula = "CompressedBackupSize / 1024 / 1024" + } + + $cmb = $dataTable.Columns.Add("CompressedBackupSizeMB", [int]) + $cmb.Expression = $formula + $cgb = $dataTable.Columns.Add("CompressedBackupSizeGB") + $cgb.Expression = "CompressedBackupSizeMB / 1024" + + $null = $dataTable.Columns.Add("SqlVersion") + + $null = $dataTable.Columns.Add("BackupPath") + + foreach ($row in $dataTable) { + $row.BackupPath = $Path + $restore.FileNumber = $row.Position + <# Select-Object does a quick and dirty conversion from datatable to PS object #> + $row.FileList = $restore.ReadFileList($server) | Select-Object * + } + $dataTable + } + } + + process { + if (Test-FunctionInterrupt) { return } + + #Extract fullnames from the file system objects + $pathStrings = @() + foreach ($pathItem in $Path) { + if ($null -ne $pathItem.FullName) { + $pathStrings += $pathItem.FullName + } + else { + $pathStrings += $pathItem + } + } + #Group by filename + $pathGroup = $pathStrings | Group-Object -NoElement | Select-Object -ExpandProperty Name + + $pathCount = ($pathGroup | Measure-Object).Count + Write-Message -Level Verbose -Message "$pathCount unique files to scan." + Write-Message -Level Verbose -Message "Checking accessibility for all the files." + + $testPath = Test-DbaSqlPath -SqlInstance $server -Path $pathGroup + + #Setup initial session state + $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + #Create Runspace pool, min - 1, max - 10 sessions: there is internal SQL Server queue for the restore operations. 10 threads seem to perform best + $runspacePool = [runspacefactory]::CreateRunspacePool(1, 10, $InitialSessionState, $Host) + $runspacePool.Open() + + $threads = @() + + foreach ($file in $pathGroup) { + if ($file -like 'http*') { + $deviceType = 'URL' + } + else { + $deviceType = 'FILE' + } + if ($pathCount -eq 1) { + $fileExists = $testPath + } + else { + $fileExists = ($testPath | Where-Object FilePath -eq $file).FileExists + } + if ($fileExists -or $deviceType -eq 'URL') { + #Create parameters hashtable + $argsRunPool = @{ + SqlInstance = $server + Path = $file + AzureCredential = $AzureCredential + DeviceType = $deviceType + } + Write-Message -Level Verbose -Message "Scanning file $file." + #Create new runspace thread + $thread = [powershell]::Create() + $thread.RunspacePool = $runspacePool + $thread.AddScript($getHeaderScript) | Out-Null + $thread.AddParameters($argsRunPool) | Out-Null + #Start the thread + $handle = $thread.BeginInvoke() + $threads += [pscustomobject]@{ + handle = $handle + thread = $thread + file = $file + deviceType = $deviceType + isRetrieved = $false + started = Get-Date + } + } + else { + Write-Message -Level Warning -Message "File $file does not exist or access denied. The SQL Server service account may not have access to the source directory." + } + } + #receive runspaces + while ($threads | Where-Object { $_.isRetrieved -eq $false }) { + $totalThreads = ($threads | Measure-Object).Count + $totalRetrievedThreads = ($threads | Where-Object { $_.isRetrieved -eq $true } | Measure-Object).Count + Write-Progress -Id 1 -Activity Updating -Status 'Progress' -CurrentOperation "Scanning Restore headers: $totalRetrievedThreads/$totalThreads" -PercentComplete ($totalRetrievedThreads / $totalThreads * 100) + foreach ($thread in ($threads | Where-Object { $_.isRetrieved -eq $false })) { + if ($thread.Handle.IsCompleted) { + $dataTable = $thread.thread.EndInvoke($thread.handle) + $thread.isRetrieved = $true + #Check if thread had any errors + if ($thread.thread.HadErrors) { + if ($thread.deviceType -eq 'FILE') { + Stop-Function -Message "Problem found with $($thread.file)." -Target $thread.file -ErrorRecord $thread.thread.Streams.Error -Continue + } + else { + Stop-Function -Message "Unable to read $($thread.file), check credential $AzureCredential and network connectivity." -Target $thread.file -ErrorRecord $thread.thread.Streams.Error -Continue + } + } + #Process the result of this thread + + $dbVersion = $dataTable[0].DatabaseVersion + $SqlVersion = (Convert-DbVersionToSqlVersion $dbVersion) + foreach ($row in $dataTable) { + $row.SqlVersion = $SqlVersion + if ($row.BackupName -eq "*** INCOMPLETE ***") { + Stop-Function -Message "$($thread.file) appears to be from a new version of SQL Server than $SqlInstance, skipping" -Target $thread.file -Continue + } + } + if ($Simple) { + $dataTable | Select-Object DatabaseName, BackupFinishDate, RecoveryModel, BackupSizeMB, CompressedBackupSizeMB, DatabaseCreationDate, UserName, ServerName, SqlVersion, BackupPath + } + elseif ($FileList) { + $dataTable.filelist + } + else { + $dataTable + } + + $thread.thread.Dispose() + } + } + Start-Sleep -Milliseconds 500 + } + #Close the runspace pool + $runspacePool.Close() + } } diff --git a/functions/Read-DbaTraceFile.ps1 b/functions/Read-DbaTraceFile.ps1 new file mode 100644 index 000000000000..6e1a5b657693 --- /dev/null +++ b/functions/Read-DbaTraceFile.ps1 @@ -0,0 +1,281 @@ +function Read-DbaTraceFile { + <# + .SYNOPSIS + Reads SQL Server trace files + + .DESCRIPTION + Using the fn_trace_gettable function, a trace file is read and returned as a PowerShell object + + This function returns the whole of the trace file. The information is presented in the format that the trace subsystem uses. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Path to the trace file. This path is relative to the SQL Server instance. + + .PARAMETER Database + Search for results only with specific DatabaseName. Uses IN for comparisons. + + .PARAMETER Login + Search for results only with specific Logins. Uses IN for comparisons. + + .PARAMETER Spid + Search for results only with specific Spids. Uses IN for comparisons. + + .PARAMETER EventClass + Search for results only with specific EventClasses. Uses IN for comparisons. + + .PARAMETER ObjectType + Search for results only with specific ObjectTypes. Uses IN for comparisons. + + .PARAMETER Error + Search for results only with specific Errors. Uses IN for comparisons. + + .PARAMETER EventSequence + Search for results only with specific EventSequences. Uses IN for comparisons. + + .PARAMETER TextData + Search for results only with specific TextData. Uses LIKE for comparisons. + + .PARAMETER ApplicationName + Search for results only with specific ApplicationNames. Uses LIKE for comparisons. + + .PARAMETER ObjectName + Search for results only with specific ObjectNames. Uses LIKE for comparisons. + + .PARAMETER Where + Custom where clause - use without the word "WHERE". Here are the available columns: + + TextData + BinaryData + DatabaseID + TransactionID + LineNumber + NTUserName + NTDomainName + HostName + ClientProcessID + ApplicationName + LoginName + SPID + Duration + StartTime + EndTime + Reads + Writes + CPU + Permissions + Severity + EventSubClass + ObjectID + Success + IndexID + IntegerData + ServerName + EventClass + ObjectType + NestLevel + State + Error + Mode + Handle + ObjectName + DatabaseName + FileName + OwnerName + RoleName + TargetUserName + DBUserName + LoginSid + TargetLoginName + TargetLoginSid + ColumnPermissions + LinkedServerName + ProviderName + MethodName + RowCounts + RequestID + XactSequence + EventSequence + BigintData1 + BigintData2 + GUID + IntegerData2 + ObjectID2 + Type + OwnerID + ParentName + IsSystem + Offset + SourceDatabaseID + SqlHandle + SessionLoginName + PlanHandle + GroupID + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Database master, tempdb -Path C:\traces\big.trc + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. Filters only results that have master or tempdb as the DatabaseName. + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Database master, tempdb -Path C:\traces\big.trc -TextData 'EXEC SP_PROCOPTION' + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. + Filters only results that have master or tempdb as the DatabaseName and that have 'EXEC SP_PROCOPTION' somewhere in the text. + + .EXAMPLE + Read-DbaTraceFile -SqlInstance sql2016 -Path C:\traces\big.trc -Where "LinkedServerName = 'myls' and StartTime > '5/30/2017 4:27:52 PM'" + + Reads the tracefile C:\traces\big.trc, stored on the sql2016 sql server. + Filters only results where LinkServerName = myls and StartTime is greater than '5/30/2017 4:27:52 PM'. + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2014 | Read-DbaTraceFile + + Reads every trace file on sql2014 + +#> + [CmdletBinding()] + Param ( + [parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(ValueFromPipelineByPropertyName)] + [PSCredential]$SqlCredential, + [parameter(ValueFromPipelineByPropertyName)] + [string[]]$Path, + [string[]]$Database, + [string[]]$Login, + [int[]]$Spid, + [string[]]$EventClass, + [string[]]$ObjectType, + [int[]]$Error, + [int[]]$EventSequence, + [string[]]$TextData, + [string[]]$ApplicationName, + [string[]]$ObjectName, + [string]$Where, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if ($where) { + $Where = "where $where" + } + elseif ($Database -or $Login -or $Spid -or $ApplicationName -or $EventClass -or $ObjectName -or $ObjectType -or $EventSequence -or $Error) { + + $tempwhere = @() + + if ($Database) { + $where = $database -join "','" + $tempwhere += "databasename in ('$where')" + } + + if ($Login) { + $where = $Login -join "','" + $tempwhere += "LoginName in ('$where')" + } + + if ($Spid) { + $where = $Spid -join "," + $tempwhere += "Spid in ($where)" + } + + if ($EventClass) { + $where = $EventClass -join "," + $tempwhere += "EventClass in ($where)" + } + + if ($ObjectType) { + $where = $ObjectType -join "," + $tempwhere += "ObjectType in ($where)" + } + + if ($Error) { + $where = $Error -join "," + $tempwhere += "Error in ($where)" + } + + if ($EventSequence) { + $where = $EventSequence -join "," + $tempwhere += "EventSequence in ($where)" + } + + if ($TextData) { + $where = $TextData -join "%','%" + $tempwhere += "TextData like ('%$where%')" + } + + if ($ApplicationName) { + $where = $ApplicationName -join "%','%" + $tempwhere += "ApplicationName like ('%$where%')" + } + + if ($ObjectName) { + $where = $ObjectName -join "%','%" + $tempwhere += "ObjectName like ('%$where%')" + } + + $tempwhere = $tempwhere -join " and " + $Where = "where $tempwhere" + } + } + process { + foreach ($instance in $sqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + if (Test-Bound -Parameter Path) { + $currentpath = $path + } + else { + $currentpath = $server.ConnectionContext.ExecuteScalar("Select path from sys.traces where is_default = 1") + } + + foreach ($file in $currentpath) { + Write-Message -Level Verbose -Message "Parsing $file" + + $exists = Test-DbaSqlPath -SqlInstance $server -Path $file + + if (!$exists) { + Write-Message -Level Warning -Message "Path does not exist" -Target $file + Continue + } + + $sql = "select SERVERPROPERTY('MachineName') AS ComputerName, + ISNULL(SERVERPROPERTY('InstanceName'), 'MSSQLSERVER') AS InstanceName, + SERVERPROPERTY('ServerName') AS SqlInstance, + * FROM [fn_trace_gettable]('$file', DEFAULT) $Where" + + try { + $server.Query($sql) + } + catch { + Stop-Function -Message "Error returned from SQL Server: $_" -Target $server -InnerErrorRecord $_ + } + } + } + } +} \ No newline at end of file diff --git a/functions/Read-DbaTransactionLog.ps1 b/functions/Read-DbaTransactionLog.ps1 new file mode 100644 index 000000000000..174323b8ed97 --- /dev/null +++ b/functions/Read-DbaTransactionLog.ps1 @@ -0,0 +1,103 @@ +function Read-DbaTransactionLog { + <# +.SYNOPSIS +Reads the live Transaction log from specified SQL Server Database + +.DESCRIPTION +Using the fn_dblog function, the live transaction log is read and returned as a PowerShell object + +This function returns the whole of the log. The information is presented in the format that the logging subsystem uses. + +A soft limit of 0.5GB of log as been implemented. This is based on testing. This limit can be overridden +at the users request, but please be aware that this may have an impact on your target databases and on the +system running this function + +.PARAMETER SqlInstance +A SQL Server instance to connect to + +.PARAMETER SqlCredential +A credential to use to connect to the SQL Instance rather than using Windows Authentication + +.PARAMETER Database +Database to read the transaction log of + +.PARAMETER IgnoreLimit +Switch to indicate that you wish to bypass the recommended limits of the function + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Database, Log, LogFile +Author: Stuart Moore (@napalmgram), stuart-moore.com + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +$Log = Read-DbaTransactionLog -SqlInstance sql2016 -Database MyDatabase + +Will read the contents of the transaction log of MyDatabase on SQL Server Instance sql2016 into the local PowerShell object $Log + +.EXAMPLE +$Log = Read-DbaTransactionLog -SqlInstance sql2016 -Database MyDatabase -IgnoreLimit + +Will read the contents of the transaction log of MyDatabase on SQL Server Instance sql2016 into the local PowerShell object $Log, ignoring the recommnedation of not returning more that 0.5GB of log + +#> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [parameter(Position = 0, Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [object]$Database, + [Switch]$IgnoreLimit, + [Alias('Silent')] + [switch]$EnableException + ) + + try { + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + + if (-not $server.databases[$Database]) { + Stop-Function -Message "$Database does not exist" + return + } + + if ($server.databases[$Database].Status -ne 'Normal') { + Stop-Function -Message "$Database is not in a normal State, command will not run." + return + } + + if ($IgnoreLimit) { + Write-Message -Level Verbose -Message "Please be aware that ignoring the recommended limits may impact on the performance of the SQL Server database and the calling system" + } + else { + #Warn if more than 0.5GB of live log. Dodgy conversion as SMO returns the value in an unhelpful format :( + $SqlSizeCheck = "select + sum(FileProperty(sf.name,'spaceused')*8/1024) as 'SizeMb' + from sys.sysfiles sf + where CONVERT(INT,sf.status & 0x40) / 64=1" + $TransLogSize = $server.Query($SqlSizeCheck, $Database) + if ($TransLogSize.SizeMb -ge 500) { + Stop-Function -Message "$Database has more than 0.5 Gb of live log data, returning this may have an impact on the database and the calling system. If you wish to proceed please rerun with the -IgnoreLimit switch" + return + } + } + + $sql = "select * from fn_dblog(NULL,NULL)" + Write-Message -Level Debug -Message $sql + Write-Message -Level Verbose -Message "Starting Log retrieval" + $server.Query($sql, $Database) + +} diff --git a/functions/Read-DbaXEFile.ps1 b/functions/Read-DbaXEFile.ps1 new file mode 100644 index 000000000000..027e6c122462 --- /dev/null +++ b/functions/Read-DbaXEFile.ps1 @@ -0,0 +1,145 @@ +function Read-DbaXEFile { + <# + .SYNOPSIS + Read XEvents from a xel or xem file. + + .DESCRIPTION + Read XEvents from a xel or xem file. + + .PARAMETER Path + The path to the xel or xem file. This is relative to the computer executing the command. UNC paths are supported. + + .PARAMETER Exact + If this switch is enabled, only an exact search will be used for the Path. By default, this command will add a wildcard to the Path because Eventing uses the file name as a template and adds characters. + + .PARAMETER Raw + If this switch is enabled, the Microsoft.SqlServer.XEvent.Linq.PublishedEvent enumeration object will be returned. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Read-DbaXEFile + + .EXAMPLE + Read-DbaXEFile -Path C:\temp\deadocks.xel + + Returns events from C:\temp\deadocks.xel. + + .EXAMPLE + Get-ChildItem C:\temp\xe\*.xel | Read-DbaXEFile + + Returns events from all .xel files in C:\temp\xe. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2014 -Session deadlocks | Read-DbaXEFile + + Reads remote XEvents by accessing the file over the admin UNC share. + + #> + [CmdletBinding()] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [Alias('FullName')] + [object[]]$Path, + [switch]$Exact, + [switch]$Raw, + [switch][Alias('Silent')] + $EnableException + ) + process { + foreach ($file in $path) { + # in order to ensure CSV gets all fields, all columns will be + # collected and output in the first (all all subsequent) object + $columns = @("name", "timestamp") + + if ($file -is [System.String]) { + $currentfile = $file + $manualadd = $true + } + elseif ($file -is [System.IO.FileInfo]) { + $currentfile = $file.FullName + $manualadd = $true + } + else { + if ($file -isnot [Microsoft.SqlServer.Management.XEvent.Session]) { + Stop-Function -Message "Unsupported file type." + return + } + + if ($file.TargetFile.Length -eq 0) { + Stop-Function -Message "This session does not have an associated Target File." + return + } + + $instance = [dbainstance]$file.ComputerName + + if ($instance.IsLocalHost) { + $currentfile = $file.TargetFile + } + else { + $currentfile = $file.RemoteTargetFile + } + } + + if (-not $Exact) { + $currentfile = $currentfile.Replace('.xel', '*.xel') + $currentfile = $currentfile.Replace('.xem', '*.xem') + + if ($currentfile -notmatch "xel" -and $currentfile -notmatch "xem") { + $currentfile = "$currentfile*.xel" + } + } + + $accessible = Test-Path -Path $currentfile + $whoami = whoami + + if (-not $accessible) { + if ($file.Status -eq "Stopped") { continue } + Stop-Function -Continue -Message "$currentfile cannot be accessed from $($env:COMPUTERNAME). Does $whoami have access?" + } + + if ($raw) { + return New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + } + + $enum = New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile) + $newcolumns = ($enum.Fields.Name | Select-Object -Unique) + + $actions = ($enum.Actions.Name | Select-Object -Unique) + foreach ($action in $actions) { + $newcolumns += ($action -Split '\.')[-1] + } + + $newcolumns = $newcolumns | Sort-Object + $columns = ($columns += $newcolumns) | Select-Object -Unique + + # Make it selectable, otherwise it's a weird enumeration + foreach ($event in (New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($currentfile))) { + $hash = [ordered]@{ } + + foreach ($column in $columns) { + $null = $hash.Add($column, $event.$column) + } + + foreach ($action in $event.Actions) { + $hash[$action.Name] = $action.Value + } + + foreach ($field in $event.Fields) { + $hash[$field.Name] = $field.Value + } + + [pscustomobject]$hash + } + } + } +} \ No newline at end of file diff --git a/functions/Register-DbaConfig.ps1 b/functions/Register-DbaConfig.ps1 new file mode 100644 index 000000000000..8de95b93385e --- /dev/null +++ b/functions/Register-DbaConfig.ps1 @@ -0,0 +1,174 @@ +function Register-DbaConfig { + <# + .SYNOPSIS + Registers an existing configuration object in registry. + + .DESCRIPTION + Registers an existing configuration object in registry. + This allows simple persisting of settings across powershell consoles. + It also can be used to generate a registry template, which can then be used to create policies. + + .PARAMETER Config + The configuration object to write to registry. + Can be retrieved using Get-DbaConfig. + + .PARAMETER FullName + The full name of the setting to be written to registry. + + .PARAMETER Module + The name of the module, whose settings should be written to registry. + + .PARAMETER Name + Default: "*" + Used in conjunction with the -Module parameter to restrict the number of configuration items written to registry. + + .PARAMETER Scope + Default: UserDefault + Who will be affected by this export how? Current user or all? Default setting or enforced? + Legal values: UserDefault, UserMandatory, SystemDefault, SystemMandatory + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Config, Module + Author: Friedrich Weinmann + + .EXAMPLE + PS C:\> Get-DbaConfig message.* | Register-DbaConfig + + Retrieves all configuration items that that start with message. and registers them in registry for the current user. + + .EXAMPLE + PS C:\> Register-DbaConfig -FullName "developer.mode.enable" -Scope SystemDefault + + Retrieves the configuration item "developer.mode.enable" and registers it in registry as the default setting for all users on this machine. + + .EXAMPLE + PS C:\> Register-DbaConfig -Module message -Scope SystemMandatory + + Retrieves all configuration items of the module MyModule, then registers them in registry to enforce them for all users on the current system. + #> + [CmdletBinding(DefaultParameterSetName = "Default")] + Param ( + [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Configuration.Config[]] + $Config, + + [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] + [string[]] + $FullName, + + [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)] + [string] + $Module, + + [Parameter(ParameterSetName = "Name", Position = 1)] + [string] + $Name = "*", + + [Sqlcollaborative.Dbatools.Configuration.ConfigScope] + $Scope = "UserDefault", + + [switch] + $EnableException + ) + + begin { + $parSet = $PSCmdlet.ParameterSetName + + function Write-Config { + [CmdletBinding()] + Param ( + [Sqlcollaborative.Dbatools.Configuration.Config] + $Config, + + [Sqlcollaborative.Dbatools.Configuration.ConfigScope] + $Scope, + + [bool] + $EnableException, + + [string] + $FunctionName = (Get-PSCallStack)[0].Command + ) + + if (-not $Config -or ($Config.RegistryData -eq "")) { + Stop-Function -Message "Invalid Input, cannot export $($Config.FullName), type not supported" -EnableException $EnableException -Category InvalidArgument -Target $Config -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "config", "fail" + return + } + + try { + Write-Message -Level Verbose -Message "Registering $($Config.FullName) for $Scope" -Target $Config -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "Config" + #region User Default + if (1 -band $Scope) { + Ensure-RegistryPath -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -ErrorAction Stop + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion User Default + + #region User Mandatory + if (2 -band $Scope) { + Ensure-RegistryPath -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -ErrorAction Stop + Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion User Mandatory + + #region System Default + if (4 -band $Scope) { + Ensure-RegistryPath -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -ErrorAction Stop + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Default" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion System Default + + #region System Mandatory + if (8 -band $Scope) { + Ensure-RegistryPath -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -ErrorAction Stop + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\dbatools\Config\Enforced" -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop + } + #endregion System Mandatory + } + catch { + Stop-Function -Message "Failed to export $($Config.FullName), to scope $Scope" -EnableException $EnableException -Target $Config -ErrorRecord $_ -FunctionName $FunctionName #-ModuleName "PSFramework" -Tag "config", "fail" + return + } + } + + function Ensure-RegistryPath { + [CmdletBinding()] + Param ( + [string] + $Path + ) + + if (-not (Test-Path $Path)) { + $null = New-Item $Path -Force -ErrorAction Stop + } + } + } + process { + switch ($parSet) { + "Default" { + foreach ($item in $Config) { + Write-Config -Config $item -Scope $Scope -EnableException $EnableException + } + + foreach ($item in $FullName) { + if ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.ContainsKey($item.ToLower())) { + Write-Config -Config ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$item.ToLower()]) -Scope $Scope -EnableException $EnableException + } + } + } + "Name" { + foreach ($item in ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name)) { + Write-Config -Config $item -Scope $Scope -EnableException $EnableException + } + } + } + } + end { + + } +} diff --git a/functions/Remove-DbaAgentJob.ps1 b/functions/Remove-DbaAgentJob.ps1 new file mode 100644 index 000000000000..f2595b4e27fe --- /dev/null +++ b/functions/Remove-DbaAgentJob.ps1 @@ -0,0 +1,152 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentJob { + <# + .SYNOPSIS + Remove-DbaAgentJob removes a job. + + .DESCRIPTION + Remove-DbaAgentJob removes a a job in the SQL Server Agent. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job. Can be null if the the job id is being used. + + .PARAMETER KeepHistory + Specifies to keep the history for the job. By default history is deleted. + + .PARAMETER KeepUnusedSchedule + Specifies to keep the schedules attached to this job if they are not attached to any other job. + By default the unused schedule is deleted. + + .PARAMETER Mode + Default: Strict + How strict does the command take lesser issues? + Strict: Interrupt if the job specified doesn't exist. + Lazy: Silently skip over jobs that don't exist. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Agent, Job + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaAgentJob + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 + + Removes the job from the instance with the name Job1 + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 -KeepHistory + + Removes the job but keeps the history + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1 -Job Job1 -KeepUnusedSchedule + + Removes the job but keeps the unused schedules + + .EXAMPLE + Remove-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job Job1 + + Removes the job from multiple servers + + .EXAMPLE + sql1, sql2, sql3 | Remove-DbaAgentJob -Job Job1 + + Removes the job from multiple servers using pipe line + + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $false)] + [switch]$KeepHistory, + [Parameter(Mandatory = $false)] + [switch]$KeepUnusedSchedule, + [DbaMode]$Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + Write-Message -Level Verbose -Message "Processing job $j" + + if ($Server.JobServer.Jobs.Name -notcontains $j) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Job $j doesn't exists on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Job $j doesn't exist on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + if ($PSCmdlet.ShouldProcess($instance, "Removing the job $j")) { + try { + $currentJob = $Server.JobServer.Jobs[$j] + $dropHistory = 1 + $dropSchedule = 1 + if (Test-Bound -ParameterName KeepHistory) { + Write-Message -Level SomewhatVerbose -Message "Job history will be kept" + $dropHistory = 0 + } + if (Test-Bound -ParameterName KeepUnusedSchedule) { + Write-Message -Level SomewhatVerbose -Message "Unused job schedules will be kept" + $dropSchedule = 0 + } + Write-Message -Level SomewhatVerbose -Message "Removing job" + $dropJobQuery = ("EXEC dbo.sp_delete_job @job_name = '{0}', @delete_history = {1}, @delete_unused_schedule = {2}" -f $currentJob.Name, $dropHistory, $dropSchedule) + $server.Databases['msdb'].ExecuteNonQuery($dropJobQuery) + } + catch { + Stop-Function -Message "Something went wrong removing the job" -Target $instance -ErrorRecord $_ -Continue + } + } + } + } + } + } + end { + Write-Message -Message "Finished removing jobs(s)." -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Remove-DbaAgentJobCategory.ps1 b/functions/Remove-DbaAgentJobCategory.ps1 new file mode 100644 index 000000000000..49cd9165b13b --- /dev/null +++ b/functions/Remove-DbaAgentJobCategory.ps1 @@ -0,0 +1,122 @@ +function Remove-DbaAgentJobCategory { + + <# +.SYNOPSIS +Remove-DbaAgentJobCategory removes a job category. + +.DESCRIPTION +Remove-DbaAgentJobCategory makes it possible to remove a job category. +Be assured that the category you want to remove is not used with other jobs. If another job uses this category it will be get the category [Uncategorized (Local)]. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaAgentJobCategory + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' + +Remove the job category Category 1 from the instance. + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1 -Category Category1, Category2, Category3 + +Remove multiple job categories from the instance. + +.EXAMPLE +Remove-DbaAgentJobCategory -SqlInstance sql1, sql2, sql3 -Category Category1, Category2, Category3 + +Remove multiple job categories from the multiple instances. + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Loop through each of the categories + foreach ($cat in $Category) { + + # Check if the job category exists + if ($cat -notin $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat doesn't exist on $instance" -Target $instance -Continue + } + + # Remove the category + if ($PSCmdlet.ShouldProcess($instance, "Changing the job category $Category")) { + try { + # Get the category + $currentCategory = $server.JobServer.JobCategories[$cat] + + Write-Message -Message "Removing job category $cat" -Level Verbose + + $currentCategory.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } #if should process + + } # for each category + + } # for each instance + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished removing job category." -Level Verbose + } + +} \ No newline at end of file diff --git a/functions/Remove-DbaAgentJobStep.ps1 b/functions/Remove-DbaAgentJobStep.ps1 new file mode 100644 index 000000000000..4b33a1483c2a --- /dev/null +++ b/functions/Remove-DbaAgentJobStep.ps1 @@ -0,0 +1,145 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentJobStep { + <# + .SYNOPSIS + Removes a step from the specified SQL Agent job. + + .DESCRIPTION + Removes a job step from a SQL Server Agent job. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + The name of the job. + + .PARAMETER StepName + The name of the job step. + + .PARAMETER Mode + Default: Strict + How strict does the command take lesser issues? + Strict: Interrupt if the configuration already has the same value as the one specified. + Lazy: Silently skip over instances that already have this configuration at the specified value. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Agent, Job, JobStep + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaAgentJobStep + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 + + Remove 'Step1' from job 'Job1' on sql1. + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1 -Job Job1, Job2, Job3 -StepName Step1 + + Remove the job step from multiple jobs. + + .EXAMPLE + Remove-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 + + Remove the job step from the job on multiple servers. + + .EXAMPLE + sql1, sql2, sql3 | Remove-DbaAgentJobStep -Job Job1 -StepName Step1 + + Remove the job step from the job on multiple servers using pipeline. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [DbaMode]$Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $instance" + + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + Write-Message -Level Verbose -Message "Processing job $j" + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Job $j doesn't exists on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Job $j doesnn't exist on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + # Check if the job step exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + switch ($Mode) { + 'Lazy' { + Write-Message -Level Verbose -Message "Step $StepName doesn't exist for $job on $instance." -Target $instance + } + 'Strict' { + Stop-Function -Message "Step $StepName doesn't exist for $job on $instance." -Continue -ContinueLabel main -Target $instance -Category InvalidData + } + } + } + else { + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Removing the job step $StepName for job $j")) { + try { + $JobStep = $Server.JobServer.Jobs[$j].JobSteps[$StepName] + Write-Message -Level SomewhatVerbose -Message "Removing the job step $StepName for job $j." + $JobStep.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job step" -Target $JobStep -Continue -ErrorRecord $_ + Write-Message -Level Verbose -Message "Could not remove the job step $StepName from $j" + } + } + } + } + } + } + } + end { + Write-Message -Message "Finished removing the jobs step(s)" -Level Verbose + } +} diff --git a/functions/Remove-DbaAgentSchedule.ps1 b/functions/Remove-DbaAgentSchedule.ps1 new file mode 100644 index 000000000000..376540bea570 --- /dev/null +++ b/functions/Remove-DbaAgentSchedule.ps1 @@ -0,0 +1,191 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaAgentSchedule { + <# +.SYNOPSIS +Remove-DbaAgentJobSchedule removes a job schedule. + +.DESCRIPTION +Remove-DbaAgentJobSchedule removes a a job in the SQL Server Agent. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Schedule +The name of the job schedule. + +.PARAMETER InputObject +A collection of schedule (such as returned by Get-DbaAgentSchedule), to be removed. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. +It will also remove the any present schedules with the same name for the specific job. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, Schedule + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaAgentJobSchedule + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule weekly +Remove the schedule weekly + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule weekly -Force +Remove the schedule weekly from the job even if the schedule is being used by another job. + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1 -Schedule daily, weekly +Remove multiple schedule + +.EXAMPLE +Remove-DbaAgentSchedule -SqlInstance sql1, sql2, sql3 -Schedule daily, weekly +Remove the schedule on multiple servers for multiple schedules + +.EXAMPLE +sql1, sql2, sql3 | Remove-DbaAgentSchedule -Schedule daily, weekly +Remove the schedule on multiple servers using pipe line + +.EXAMPLE +Get-DbaAgentSchedule -SqlInstance sql1 -Schedule sched1, sched2, sched3 | Remove-DbaAgentSchedule + +Remove the schedules using a pipeline + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential] + $SqlCredential, + [Parameter(Mandatory = $true, ParameterSetName = "instance")] + [ValidateNotNullOrEmpty()] + [Alias("Schedules")] + [object[]]$Schedule, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "schedules")] + [Microsoft.SqlServer.Management.Smo.Agent.ScheduleBase[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException, + [switch]$Force + ) + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $InputObject += $server.JobServer.SharedSchedules | Where-Object { $_.Name -in $Schedule } + + } # foreach object instance + + foreach ($s in $InputObject) { + + if ($Server.JobServer.SharedSchedules.Name -contains $s.Name) { + # Get job count + $jobCount = $Server.JobServer.SharedSchedules[$s].JobCount + + # Check if the schedule is shared among other jobs + if ($jobCount -ge 1 -and -not $Force) { + Stop-Function -Message "The schedule $s is shared connected to one or more jobs. If removal is neccesary use -Force." -Target $instance -Continue + } + + # Remove the job schedule + if ($PSCmdlet.ShouldProcess($instance, "Removing schedule $s on $instance")) { + # Loop through each of the schedules and drop them + Write-Message -Message "Removing schedule $s on $instance" -Level Verbose + + #Check if jobs use the schedule + if ($jobCount -ge 1) { + # Get the job object + $smoSchedules = $server.JobServer.SharedSchedules | Where-Object {($_.Name -eq $s.Name)} + + Write-Message -Message "Schedule $sched is used in one or more jobs. Removing it for each job." -Level Verbose + + # Loop through each if the schedules + foreach ($smoSchedule in $smoSchedules) { + + # Get the job ids + $jobGuids = $Server.JobServer.SharedSchedules[$smoSchedule].EnumJobReferences() + + if (($jobCount -gt 1 -and $Force) -or $jobCount -eq 1) { + + # Loop though each of the jobs + foreach ($guid in $jobGuids) { + # Get the job object + $smoJob = $Server.JobServer.GetJobByID($guid) + + # Get the job schedule + $jobSchedules = $Server.JobServer.Jobs[$smoJob].JobSchedules | Where-Object {$_.Name -eq $smoSchedule} + + foreach ($jobSchedule in $jobSchedules) { + try { + Write-Message -Message "Removing the schedule $jobSchedule for job $smoJob" -Level Verbose + + $jobSchedule.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the job schedule" -Target $instance -ErrorRecord $_ -Continue + } + } + } # foreach guid + } # if jobcount + + } # foreach smoschedule + } # if jobcount ge 1 + + Write-Message -Message "Removing schedules that are not being used by other jobs." -Level Verbose + + # Get the schedules + $smoSchedules = $server.JobServer.SharedSchedules | Where-Object {($_.Name -eq $s.Name) -and ($_.JobCount -eq 0)} + + # Remove the schedules that have no jobs + foreach ($smoSchedule in $smoSchedules) { + try { + $smoSchedule.Drop() + } + catch { + Stop-Function -Message "Something went wrong removing the schedule" -Target $instance -ErrorRecord $_ -Continue + } + } # foreach schedule + } # should process + } # if contains schedule + else { + Stop-Function -Message "Schedule $s is not present on instance $instance" -Target $instance -Continue + } + } #foreach object schedule + + } # process + + end { + Write-Message -Message "Finished removing jobs schedule(s)." -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Remove-DbaBackup.ps1 b/functions/Remove-DbaBackup.ps1 index 5d76d2ed4c66..23c8c38d49b3 100644 --- a/functions/Remove-DbaBackup.ps1 +++ b/functions/Remove-DbaBackup.ps1 @@ -1,257 +1,157 @@ -Function Remove-DbaBackup -{ -<# -.SYNOPSIS -Remove SQL Server backups from disk +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaBackup { + <# + .SYNOPSIS + Removes SQL Server backups from disk. -.DESCRIPTION -Provides all of the same functionality for removing SQL backups from disk as a standard maintenance plan would. + .DESCRIPTION + Provides all of the same functionality for removing SQL backups from disk as a standard maintenance plan would. -As an addition you have the ability to check the Archive bit on files before deletion. This will allow you to ensure -backups have been archived to your archive location before removal. + As an addition you have the ability to check the Archive bit on files before deletion. This will allow you to ensure backups have been archived to your archive location before removal. -Also included is the ability to remove empty folders as part of this cleanup activity. + Also included is the ability to remove empty folders as part of this cleanup activity. -.PARAMETER Path -Name of the base level folder to search for backup files. -Deletion of backup files will be recursive from this location. + .PARAMETER Path + Specifies the name of the base level folder to search for backup files. Deletion of backup files will be recursive from this location. -.PARAMETER BackupFileExtension -Extension of the backup files you wish to remove (typically bak or log) + .PARAMETER BackupFileExtension + Specifies the filename extension of the backup files you wish to remove (typically 'bak', 'trn' or 'log'). Do not include the period. -.PARAMETER RetentionPeriod -Retention period for backup files. Correct format is ##U. + .PARAMETER RetentionPeriod + Specifies the retention period for backup files. Correct format is ##U. -## is the retention value and must be an integer value -U signifies the units where the valid units are: -h = hours -d = days -w = weeks -m = months + ## is the retention value and must be an integer value + U signifies the units where the valid units are: + h = hours + d = days + w = weeks + m = months -Formatting Examples: -'48h' = 48 hours -'7d' = 7 days -'4w' = 4 weeks -'1m' = 1 month + Formatting Examples: + '48h' = 48 hours + '7d' = 7 days + '4w' = 4 weeks + '1m' = 1 month -.PARAMETER CheckArchiveBit -Check the archive bit on files before deletion + .PARAMETER CheckArchiveBit + If this switch is enabled, the filesystem Archive bit is checked before deletion. If this bit is set (which translates to "it has not been backed up to another location yet", the file won't be deleted. -.PARAMETER RemoveEmptyBackupFolder -Remove any empty folders after the cleanup process is complete. + .PARAMETER RemoveEmptyBackupFolder + If this switch is enabled, empty folders will be removed after the cleanup process is complete. -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. -.NOTES -Tags: Storage, DisasterRecovery, Backup -Original Author: Chris Sommer, @cjsommer, www.cjsommer.com + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.i -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire + .NOTES + Tags: Storage, DisasterRecovery, Backup + Author: Chris Sommer, @cjsommer, www.cjsommer.com -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + .LINK + https://dbatools.io/Remove-DbaBackup -You should have received a copy of the GNU General Public License -along with this program. If not, see . + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h -.LINK -https://dbatools.io/Remove-DbaBackup + '*.trn' files in 'C:\MSSQL\SQL Backup\' and all subdirectories that are more than 48 hours old will be removed. -.EXAMPLE -Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h -WhatIf -The cmdlet will remove '*.trn' files from 'C:\MSSQL\SQL Backup\' and all subdirectories that are more than 48 hours. + Same as example #1, but doesn't actually remove any files. The function will instead show you what would be done. + This is useful when first experimenting with using the function. -.EXAMPLE -Remove-DbaBackup -Path 'C:\MSSQL\SQL Backup\' -BackupFileExtension trn -RetentionPeriod 48h -WhatIf - -Same as example #1, but using the WhatIf parameter. The WhatIf parameter will allow the cmdlet show you what it will do, without actually doing it. -In this case, no trn files will be deleted. Instead, the cmdlet will output what it will do when it runs. This is a good preventatitive measure -especially when you are first configuring the cmdlet calls. + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 7d -CheckArchiveBit -.EXAMPLE -Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 7d -CheckArchiveBit + '*.bak' files in 'C:\MSSQL\Backup\' and all subdirectories that are more than 7 days old will be removed, but only if the files have been backed up to another location as verified by checking the Archive bit. -The cmdlet will remove '*.bak' files from 'C:\MSSQL\Backup\' and all subdirectories that are more than 7 days old. -It will also ensure that the bak files have been archived using the archive bit before removing them. - -.EXAMPLE -Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 1w -RemoveEmptyBackupFolder - -The cmdlet will remove '*.bak' files from 'C:\MSSQL\Backup\' and all subdirectories that are more than 1 week old. -It will also remove any backup folders that no longer contain backup files. - - -#> - [CmdletBinding(SupportsShouldProcess=$true)] - Param ( - [parameter(Mandatory = $true,HelpMessage="Full path to the root level backup folder (ex. 'C:\SQL\Backups'")] - [Alias("BackupFolder")] - [ValidateScript({Test-Path $_ -PathType 'Container'})] - [string]$Path, - - [parameter(Mandatory = $true,HelpMessage="Backup File extension to remove (ex. bak, trn, dif)")] - [string]$BackupFileExtension , - - [parameter(Mandatory = $true,HelpMessage="Backup retention period. (ex. 24h, 7d, 4w, 6m)")] - [string]$RetentionPeriod , - - [parameter(Mandatory = $false)] - [switch]$CheckArchiveBit = $false , + .EXAMPLE + Remove-DbaBackup -Path 'C:\MSSQL\Backup\' -BackupFileExtension bak -RetentionPeriod 1w -RemoveEmptyBackupFolder + '*.bak' files in 'C:\MSSQL\Backup\' and all subdirectories that are more than 1 week old will be removed. Any folders left empty will be removed as well. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, HelpMessage = "Full path to the root level backup folder (ex. 'C:\SQL\Backups'")] + [Alias("BackupFolder")] + [string]$Path, + [parameter(Mandatory = $true, HelpMessage = "Backup File extension to remove (ex. bak, trn, dif)")] + [string]$BackupFileExtension , + [parameter(Mandatory = $true, HelpMessage = "Backup retention period. (ex. 24h, 7d, 4w, 6m)")] + [string]$RetentionPeriod , [parameter(Mandatory = $false)] - [switch]$RemoveEmptyBackupFolder = $false - ) - - BEGIN - { - ### Local Functions - function Convert-UserFriendlyRetentionToDatetime - { - [cmdletbinding()] - param ( - [string]$UserFriendlyRetention - ) - - <# - Convert a user friendly retention value into a datetime. - The last character of the string will indicate units (validated) - Valid units are: (h = hours, d = days, w = weeks, m = months) - - The preceeding characters are the value and must be an integer (validated) - - Examples: - '48h' = 48 hours - '7d' = 7 days - '4w' = 4 weeks - '1m' = 1 month - #> - - [int]$Length = ($UserFriendlyRetention).Length - $Value = ($UserFriendlyRetention).Substring(0,$Length-1) - $Units = ($UserFriendlyRetention).Substring($Length-1,1) - - # Validate that $Units is an accepted unit of measure - if ( $Units -notin @('h','d','w','m') ){ - throw "RetentionPeriod '$UserFriendlyRetention' units invalid! See Get-Help for correct formatting and examples." - } - - # Validate that $Value is an INT - if ( ![int]::TryParse($Value,[ref]"") ) { - throw "RetentionPeriod '$UserFriendlyRetention' format invalid! See Get-Help for correct formatting and examples." - } - - switch ($Units) - { - 'h' { $UnitString = 'Hours'; [datetime]$ReturnDatetime = (Get-Date).AddHours(-$Value) } - 'd' { $UnitString = 'Days'; [datetime]$ReturnDatetime = (Get-Date).AddDays(-$Value) } - 'w' { $UnitString = 'Weeks'; [datetime]$ReturnDatetime = (Get-Date).AddDays(-$Value*7) } - 'm' { $UnitString = 'Months';[datetime]$ReturnDatetime = (Get-Date).AddMonths(-$Value) } - } - Write-Verbose "Retention set to '$Value' $UnitString. Retention date/time '$ReturnDatetime'" - $ReturnDatetime - } - - function Get-EmptyBackupFolder - { - [cmdletbinding()] - param ( - [string]$BaseLocation - ) - Get-ChildItem -Path $BaseLocation -Recurse | Where-Object {$_.PSIsContainer -eq $true -and (Get-ChildItem -Path $_.FullName) -eq $null} - } - - - # Validations + [switch]$CheckArchiveBit = $false , + [parameter(Mandatory = $false)] + [switch]$RemoveEmptyBackupFolder = $false, + [Alias('Silent')] + [switch]$EnableException + ) + begin { # Ensure BackupFileExtension does not begin with a . if ($BackupFileExtension -match "^[.]") { - Write-Warning "Parameter -BackupFileExtension begins with a period '$BackupFileExtension'. A period is automatically prepended to -BackupFileExtension and need not be passed in." - } - - # Initialize stuff - $Start = Get-Date - } - PROCESS - { - # Process stuff - Write-Output ("Started at $Start") - Write-Output ("Removing backups from $Path") - - # Convert Retention Value to an actual DateTime - try { - $RetentionDate = Convert-UserFriendlyRetentionToDatetime -UserFriendlyRetention $RetentionPeriod - Write-Output "Backup Retention Date set to $RetentionDate" - } catch { - throw $_ - } - - # Generate list of files that are to be removed - $FilesToDelete = Get-ChildItem "$Path" -Filter "*.$BackupFileExtension" -Recurse | ` - Where-Object {$_.LastWriteTime -lt $RetentionDate} - - # Filter out unarchived files if -CheckArchiveBit parameter is used - if ($CheckArchiveBit.IsPresent) { - Write-Output 'Removing only archived files' - $FilesToDelete = $FilesToDelete | Where-Object {$_.attributes -notmatch "Archive"} + Write-Message -Level Warning -Message "Parameter -BackupFileExtension begins with a period '$BackupFileExtension'. A period is automatically prepended to -BackupFileExtension and need not be passed in." } - - # Perform the deletion or show which file will be deleted if WhatIf is used - foreach ($file in $filestodelete) { - If ($Pscmdlet.ShouldProcess( $(($file.FullName).Replace($file.Name, '')), "Removing backup file '$($file.name)'")) { + } + process { + # Process stuff + Write-Message -Message "Removing backups from $Path" -Level Verbose + Find-DbaBackup -Path $Path -BackupFileExtension $BackupFileExtension -RetentionPeriod $RetentionPeriod -CheckArchiveBit:$CheckArchiveBit -EnableException | + Foreach-Object { + $file = $_ + if ($PSCmdlet.ShouldProcess($file.Directory.FullName, "Removing backup file $($file.Name)")) { try { - Write-Output "Removing backup file '$($file.FullName)'" - $file.FullName | Remove-Item -Force - } catch { - throw $_ + $file | Remove-Item -Force -EA Stop + } + catch { + Write-Message -Message "Failed to remove $file." -Level Warning -ErrorRecord $_ } } } - - # Cleanup empty backup folders. Using a DO/WHILE loop to force it to go through the logic at least once so we can display WhatIf if we need to - do { - if ($RemoveEmptyBackupFolder.IsPresent) { - # Get the empty backup folders - $EmptyBackupFolder = Get-EmptyBackupFolder -BaseLocation $Path - - foreach ($folder in $EmptyBackupFolder) { - if ($Pscmdlet.ShouldProcess($Path, "Removing empty folder '.$(($folder.FullName).Replace($Path,''))")) { - try { - Write-Output "Removing empty folder '$($folder.FullName)'" - $folder.FullName | Remove-Item -Force - } catch { - throw $_ - } + Write-Message -Message "File Cleaning ended." -Level Verbose + # Cleanup empty backup folders. + if ($RemoveEmptyBackupFolder) { + Write-Message -Message "Removing empty folders." -Level Verbose + (Get-ChildItem -Directory -Path $Path -Recurse -ErrorAction SilentlyContinue -ErrorVariable EnumErrors).FullName | + Sort-Object -Descending | + Foreach-Object { + $OrigPath = $_ + try { + $Contents = @(Get-ChildItem -Force $OrigPath -ErrorAction Stop) + } + catch { + Write-Message -Message "Can't enumerate $OrigPath." -Level Warning -ErrorRecord $_ + } + if ($Contents.Count -eq 0) { + return $_ + } + } | + Foreach-Object { + $FolderPath = $_ + if ($PSCmdlet.ShouldProcess($Path, "Removing empty folder .$($FolderPath.Replace($Path, ''))")) { + try { + $FolderPath | Remove-Item -ErrorAction Stop + } + catch { + Write-Message -Message "Failed to remove $FolderPath." -Level Warning -ErrorRecord $_ } } } - # Refresh the empty folders. This will allow us to recursively clean them up - $EmptyBackupFolder = Get-EmptyBackupFolder -BaseLocation $Path - - } while ($RemoveEmptyBackupFolder.IsPresent -and !$WhatIfPreference -and $EmptyBackupFolder) - } - - END - { - # End cleanup - if ($Pscmdlet.ShouldProcess($env:computername, "Showing final message")) - { - $End = Get-Date - Write-Output "Finished at $End" - $Duration = $End - $start - Write-Output "Script Duration: $Duration" - } - } + if ($EnumErrors) { + Write-Message "Errors encountered enumerating folders." -Level Warning -ErrorRecord $EnumErrors + } + Write-Message -Message "Removed empty folders." -Level Verbose -EnableException $EnableException + } + } } diff --git a/functions/Remove-DbaClientAlias.ps1 b/functions/Remove-DbaClientAlias.ps1 new file mode 100644 index 000000000000..09cb6654f07c --- /dev/null +++ b/functions/Remove-DbaClientAlias.ps1 @@ -0,0 +1,128 @@ +function Remove-DbaClientAlias { + <# + .SYNOPSIS + Removes a sql alias for the specified server - mimics cliconfg.exe + + .DESCRIPTION + Removes a SQL Server alias by altering HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client + + .PARAMETER ComputerName + The target computer where the alias will be created + + .PARAMETER Credential + Allows you to login to remote computers using alternative credentials + + .PARAMETER Alias + The alias to be deleted + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Alias + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaClientAlias + + .EXAMPLE + Remove-DbaClientAlias -ComputerName workstationx -Alias sqlps + Removes the sqlps SQL client alias on workstationx + + .EXAMPLE + Get-DbaClientAlias | Remove-DbaClientAlias + Removes all SQL Server client aliases on the local computer + +#> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipelineByPropertyName)] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('AliasName')] + [string]$Alias, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($computer in $ComputerName) { + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + $scriptblock = { + $Alias = $args[0] + function Get-ItemPropertyValue { + Param ( + [parameter()] + [String]$Path, + [parameter()] + [String]$Name + ) + Get-ItemProperty -LiteralPath $Path -Name $Name + } + + $basekeys = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer", "HKLM:\SOFTWARE\Microsoft\MSSQLServer" + + foreach ($basekey in $basekeys) { + + if ((Test-Path $basekey) -eq $false) { + Write-Warning "Base key ($basekey) does not exist. Quitting." + continue + } + + $client = "$basekey\Client" + + if ((Test-Path $client) -eq $false) { + continue + } + + $connect = "$client\ConnectTo" + + if ((Test-Path $connect) -eq $false) { + continue + } + + if ($basekey -like "*WOW64*") { + $architecture = "32-bit" + } + else { + $architecture = "64-bit" + } + + + $all = Get-Item -Path $connect + foreach ($entry in $all) { + + foreach ($en in $entry) { + $e = $entry.ToString().Replace('HKEY_LOCAL_MACHINE', 'HKLM:\') + if ($en.Property -contains $Alias) { + Remove-ItemProperty -Path $e -Name $Alias + } + else { + $en + } + } + } + } + } + + if ($PScmdlet.ShouldProcess($computer, "Getting aliases")) { + try { + $null = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $scriptblock -ErrorAction Stop -Verbose:$false -ArgumentList $Alias + Get-DbaClientAlias -ComputerName $computer + + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaCmConnection.ps1 b/functions/Remove-DbaCmConnection.ps1 new file mode 100644 index 000000000000..baaaf5339219 --- /dev/null +++ b/functions/Remove-DbaCmConnection.ps1 @@ -0,0 +1,69 @@ +function Remove-DbaCmConnection { + <# + .SYNOPSIS + Removes connection objects from the connection cache used for remote computer management. + + .DESCRIPTION + Removes connection objects from the connection cache used for remote computer management. + + .PARAMETER ComputerName + The computer whose connection to remove. + Accepts both text as well as the output of Get-DbaCmConnection. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ComputerManagement, CIM + Author: Fred Winmann (@FredWeinmann) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaCmConnection + + .EXAMPLE + Remove-DbaCmConnection -ComputerName sql2014 + + Removes the cached connection to the server sql2014 from the cache. + + .EXAMPLE + Get-DbaCmConnection | Remove-DbaCmConnection + + Clears the entire connection cache. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true, Mandatory = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + } + PROCESS { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Removing from connection cache: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + if ([Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections.ContainsKey($connectionObject.Connection.ComputerName)) { + $null = [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections.Remove($connectionObject.Connection.ComputerName) + Write-Message -Level Verbose -Message "Successfully removed $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + } + else { + Write-Message -Level Verbose -Message "Not found: $($connectionObject.Connection.ComputerName)" -Target $connectionObject.Connection.ComputerName + } + } + } + END { + Write-Message -Level InternalComment -Message "Ending" + } +} diff --git a/functions/Remove-DbaComputerCertificate.ps1 b/functions/Remove-DbaComputerCertificate.ps1 new file mode 100644 index 000000000000..6843f6b71095 --- /dev/null +++ b/functions/Remove-DbaComputerCertificate.ps1 @@ -0,0 +1,116 @@ +function Remove-DbaComputerCertificate { + <# + .SYNOPSIS + Removes a computer certificate - useful for removing easily certs from remote computers + + .DESCRIPTION + Removes a computer certificate from a local or remote compuer + + .PARAMETER ComputerName + The target computer - defaults to localhost + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials + + .PARAMETER Thumbprint + The thumbprint of the certificate object + + .PARAMETER Store + Certificate store - defaults to LocalMachine (otherwise exceptions can be thrown on remote connections) + + .PARAMETER Folder + Certificate folder + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaComputerCertificate -ComputerName Server1 -Thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 + + Removes certificate with thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 in the LocalMachine store on Server1 + + .EXAMPLE + Get-DbaComputerCertificate | Where-Object Thumbprint -eq E0A071E387396723C45E92D42B2D497C6A182340 | Remove-DbaComputerCertificate + + Removes certificate using the pipeline + + .EXAMPLE + Remove-DbaComputerCertificate -ComputerName Server1 -Thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 -Store User -Folder My + + Removes certificate with thumbprint C2BBE81A94FEE7A26FFF86C2DFDAF6BFD28C6C94 in the User\My (Personal) store on Server1 + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [parameter(ValueFromPipelineByPropertyName, Mandatory)] + [string[]]$Thumbprint, + [string]$Store = "LocalMachine", + [string]$Folder = "My", + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + #region Scriptblock for remoting + $scriptblock = { + param ( + $Thumbprint, + + $Store, + + $Folder + ) + Write-Verbose "Searching Cert:\$Store\$Folder for thumbprint: $thumbprint" + $cert = Get-ChildItem "Cert:\$store\$folder" -Recurse | Where-Object { $_.Thumbprint -eq $Thumbprint } + + if ($cert) { + $null = $cert | Remove-Item + $status = "Removed" + } + else { + $status = "Certificate not found in Cert:\$Store\$Folder" + } + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + Store = $Store + Folder = $Folder + Thumbprint = $thumbprint + Status = $status + } + } + #endregion Scriptblock for remoting + } + + process { + foreach ($computer in $computername) { + foreach ($thumb in $Thumbprint) { + if ($PScmdlet.ShouldProcess("local", "Connecting to $computer to remove cert from Cert:\$Store\$Folder")) { + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ArgumentList $thumb, $Store, $Folder -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message $_ -ErrorRecord $_ -Target $computer -Continue + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaDatabase.ps1 b/functions/Remove-DbaDatabase.ps1 new file mode 100644 index 000000000000..e68d3078dd13 --- /dev/null +++ b/functions/Remove-DbaDatabase.ps1 @@ -0,0 +1,168 @@ +function Remove-DbaDatabase { + <# +.SYNOPSIS +Drops a database, hopefully even the really stuck ones. + +.DESCRIPTION +Tries a bunch of different ways to remove a database or two or more. + +.PARAMETER SqlInstance +The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Database +The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + +.PARAMETER InputObject +A collection of databases (such as returned by Get-DbaDatabase), to be removed. + +.PARAMETER IncludeSystemDb +Use this switch to disable any kind of verbose messages + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Delete, Databases + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaDatabase + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb + +Prompts then removes the database containeddb on SQL Server sql2016 + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb, mydb + +Prompts then removes the databases containeddb and mydb on SQL Server sql2016 + +.EXAMPLE +Remove-DbaDatabase -SqlInstance sql2016 -Database containeddb -Confirm:$false + +Does not prompt and swiftly removes containeddb on SQL Server sql2016 + +.EXAMPLE +Get-DbaDatabase -SqlInstance server\instance -ExcludeAllSystemDb | Remove-DbaDatabase + +Removes all the user databases from server\instance + +.EXAMPLE +Get-DbaDatabase -SqlInstance server\instance -ExcludeAllSystemDb | Remove-DbaDatabase -Confirm:$false + +Removes all the user databases from server\instance without any confirmation +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = "Default")] + Param ( + [parameter(, Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(Mandatory = $false)] + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("Databases")] + [object[]]$Database, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "databases")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [switch]$IncludeSystemDb, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Databases | Where-Object { $_.Name -in $Database } + } + + $system_dbs = @( "master", "model", "tempdb", "resource", "msdb" ) + + if (-not($IncludeSystemDb)) { + $InputObject = $InputObject | Where-Object { $_.Name -notin $system_dbs} + } + + foreach ($db in $InputObject) { + try { + $server = $db.Parent + if ($Pscmdlet.ShouldProcess("$db on $server", "KillDatabase")) { + $server.KillDatabase($db.name) + $server.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + try { + if ($Pscmdlet.ShouldProcess("$db on $server", "alter db set single_user with rollback immediate then drop")) { + $null = $server.Query("if exists (select * from sys.databases where name = '$($db.name)' and state = 0) alter database $db set single_user with rollback immediate; drop database $db") + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + try { + if ($Pscmdlet.ShouldProcess("$db on $server", "SMO drop")) { + $server.databases[$dbname].Drop() + $server.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = "Dropped" + } + } + } + catch { + Write-Message -Level Verbose -Message "Could not drop database $db on $server" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + Status = $_ + } + } + } + } + } + } +} diff --git a/functions/Remove-DbaDatabaseMasterKey.ps1 b/functions/Remove-DbaDatabaseMasterKey.ps1 new file mode 100644 index 000000000000..a26d0ee37f36 --- /dev/null +++ b/functions/Remove-DbaDatabaseMasterKey.ps1 @@ -0,0 +1,210 @@ +function Remove-DbaDatabaseMasterKey { + <# + .SYNOPSIS + Deletes specified database master key + + .DESCRIPTION + Deletes specified database master key. + + .PARAMETER SqlInstance + The target SQL Server instance. + + .PARAMETER SqlCredential + Allows you to login to SQL Server using alternative credentials. + + .PARAMETER Database + The database where the master key will be removed. + + .PARAMETER ExcludeDatabase + List of databases to exclude from clearing all master keys + + .PARAMETER All + Purge the master keys from all databases on an instance. + + .PARAMETER MasterKeyCollection + Internal parameter to support pipeline input + + .PARAMETER Mode + Controls how the function handles cases where it can't do anything due to missing database or key: + Strict: Write a warning (default) + Lazy: Write a verbose message + Report: Create a report object as part of the output + The default action can be adjusted by using Set-DbaConfig to change the 'message.mode.default' configuration + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaDatabaseMasterKey -SqlInstance Server1 + + The master key in the master database on server1 will be removed if it exists. + + .EXAMPLE + Remove-DbaDatabaseMasterKey -SqlInstance Server1 -Database db1 -Confirm:$false + + Suppresses all prompts to remove the master key in the 'db1' database and drops the key. + + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ParameterSetName = "instanceExplicit")] + [parameter(Mandatory, ParameterSetName = "instanceAll")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]] + $SqlInstance, + + [System.Management.Automation.PSCredential] + $SqlCredential, + + [parameter(Mandatory, ParameterSetName = "instanceExplicit")] + [object[]] + $Database, + + [parameter(ParameterSetName = "instanceAll")] + [object[]] + $ExcludeDatabase, + + [parameter(Mandatory, ParameterSetName = "instanceAll")] + [switch] + $All, + + [parameter(ValueFromPipeline, ParameterSetName = "collection")] + [Microsoft.SqlServer.Management.Smo.MasterKey[]] + $MasterKeyCollection, + + [DbaMode] + $Mode = (Get-DbaConfigValue -Name 'message.mode.default' -Fallback "Strict"), + + [switch] + [Alias('Silent')]$EnableException + ) + + begin { + function Drop-Masterkey { + [CmdletBinding()] + Param ( + $masterkey, + + $mode = $Mode, + + $EnableException = $EnableException + ) + $server = $masterkey.Parent.Parent + $instance = $server.DomainInstanceName + $cert = $masterkey.Name + $db = $masterkey.Parent + + if ($Pscmdlet.ShouldProcess($instance, "Dropping the master key for database '$db'")) { + try { + $masterkey.Drop() + Write-Message -Level Verbose -Message "Successfully removed master key from the $db database on $instance" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Status = "Success" + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Status = "Failure" + } + Stop-Function -Message "Failed to drop master key from $db on $instance." -Target $db -InnerErrorRecord $_ -Continue + } + } + } + } + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($All) { + $Database = ($server.Databases | Where-Object Name -NotIn $ExcludeDatabase).Name + } + + :Database foreach ($db in $Database) { + $smodb = $server.Databases[$db] + $masterkey = $smodb.MasterKey + + #region Case: Database Unknown + if ($null -eq $smodb) { + switch ($Mode) { + [DbaMode]::Strict { Stop-Function -Message "Database '$db' does not exist on $instance" -Target $smodb -Continue -ContinueLabel database } + [DbaMode]::Lazy { + Write-Message -Level (Get-DbaConfigValue -Name 'message.mode.lazymessagelevel' -Fallback 4) -Message "Database '$db' does not exist on $instance" -Target $smodb + continue database + } + [DbaMode]::Report { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + Status = "Unknown Database" + } + continue Database + } + } + } + #endregion Case: Database Unknown + + #region Case: No Master Key + if ($null -eq $masterkey) { + switch ($Mode.ToString()) { + "Strict" { Stop-Function -Message "No master key exists in the $db database on $instance" -Target $smodb -Continue -ContinueLabel database } + "Lazy" { + Write-Message -Level (Get-DbaConfigValue -Name 'message.mode.lazymessagelevel' -Fallback 4) -Message "No master key exists in the $db database on $instance" -Target $smodb + continue database + } + "Report" { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $smodb.Name + Status = "No Masterkey" + } + continue Database + } + } + } + #endregion Case: No Master Key + + Write-Message -Level Verbose -Message "Removing master key from $db" + Drop-Masterkey -masterkey $masterkey + } + } + + foreach ($key in $MasterKeyCollection) { + Write-Message -Level Verbose -Message "Removing master key: $key" + Drop-Masterkey -masterkey $key + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaDatabaseSafely.ps1 b/functions/Remove-DbaDatabaseSafely.ps1 new file mode 100644 index 000000000000..5dfe7630298e --- /dev/null +++ b/functions/Remove-DbaDatabaseSafely.ps1 @@ -0,0 +1,703 @@ +function Remove-DbaDatabaseSafely { + <# + .SYNOPSIS + Safely removes a SQL Database and creates an Agent Job to restore it. + + .DESCRIPTION + Performs a DBCC CHECKDB on the database, backs up the database with Checksum and verify only to a final (golden) backup location, creates an Agent Job to restore from that backup, drops the database, runs the agent job to restore the database, performs a DBCC CHECKDB and drops the database. + + With huge thanks to Grant Fritchey and his verify your backups video. Take a look, it's only 3 minutes long. http://sqlps.io/backuprant + + .PARAMETER SqlInstance + The SQL Server instance holding the databases to be removed. You must have sysadmin access and server version must be SQL Server version 2000 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Destination + If specified, Agent jobs will be created on this server. By default, the jobs will be created on the server specified by SqlInstance. You must have sysadmin access and the server must be SQL Server 2000 or higher. The SQL Agent service will be started if it is not already running. + + .PARAMETER DestinationCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies one or more databases to remove. + + .PARAMETER NoDbccCheckDb + If this switch is enabled, the initial DBCC CHECK DB will be skipped. This will make the process quicker but will also allow you to create an Agent job that restores a database backup containing a corrupt database. + + A second DBCC CHECKDB is performed on the restored database so you will still be notified BUT USE THIS WITH CARE. + + .PARAMETER BackupFolder + Specifies the path to a folder where the final backups of the removed databases will be stored. If you are using separate source and destination servers, you must specify a UNC path such as \\SERVER1\BACKUPSHARE\ + + .PARAMETER JobOwner + Specifies the name of the account which will own the Agent jobs. By default, sa is used. + + .PARAMETER UseDefaultFilePaths + If this switch is enabled, the default file paths for the data and log files on the instance where the database is restored will be used. By default, the original file paths will be used. + + .PARAMETER CategoryName + Specifies the Category Name for the Agent job that is created for restoring the database(s). By default, the name is "Rationalisation". + + .PARAMETER BackupCompression + If this switch is enabled, compression will be used for the backup regardless of the SQL Server instance setting. By default, the SQL Server instance setting for backup compression is used. + + .PARAMETER AllDatabases + If this switch is enabled, all user databases on the server will be removed. This is useful when decommissioning a server. You should use a DestinationServer with this switch. + + .PARAMETER ReuseSourceFolderStructure + If this switch is enabled, the source folder structure will be used when restoring instead of using the destination instance default folder structure. + + .PARAMETER Force + If this switch is enabled, all actions will be performed even if DBCC errors are detected. An Agent job will be created with 'DBCCERROR' in the name and the backup file will have 'DBCC' in its name. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Remove + Author: Rob Sewell @SQLDBAWithBeard, sqldbawithabeard.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDatabaseSafely + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -Database RideTheLightning -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on database RideTheLightning on server Fade2Black. If there are no errors, the database is backup to the folder C:\MSSQL\Backup\Rationalised - DO NOT DELETE. Then, an Agent job to restore the database from that backup is created. The database is then dropped, the Agent job to restore it run, a DBCC CHECKDB run against the restored database, and then it is dropped again. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + $Database = 'DemoNCIndex','RemoveTestDatabase' + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -Database $Database -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on two databases, 'DemoNCIndex' and 'RemoveTestDatabase' on server Fade2Black. Then, an Agent job to restore each database from those backups is created. The databases are then dropped, the Agent jobs to restore them run, a DBCC CHECKDB run against the restored databases, and then they are dropped again. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance 'Fade2Black' -DestinationServer JusticeForAll -Database RideTheLightning -BackupFolder '\\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE' + + Performs a DBCC CHECKDB on database RideTheLightning on server Fade2Black. If there are no errors, the database is backup to the folder \\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE . Then, an Agent job is created on server JusticeForAll to restore the database from that backup is created. The database is then dropped on Fade2Black, the Agent job to restore it on JusticeForAll is run, a DBCC CHECKDB run against the restored database, and then it is dropped from JusticeForAll. + + Any DBCC errors will be written to your documents folder + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance IronMaiden -Database $Database -DestinationServer TheWildHearts -BackupFolder Z:\Backups -NoDbccCheckDb -UseDefaultFilePaths -JobOwner 'THEBEARD\Rob' + + For the databases $Database on the server IronMaiden a DBCC CHECKDB will not be performed before backing up the databases to the folder Z:\Backups. Then, an Agent job is created on server TheWildHearts with a Job Owner of THEBEARD\Rob to restore each database from that backup using the instance's default file paths. The database(s) is(are) then dropped on IronMaiden, the Agent job(s) run, a DBCC CHECKDB run on the restored database(s), and then the database(s) is(are) dropped. + + .EXAMPLE + Remove-DbaDatabaseSafely -SqlInstance IronMaiden -Database $Database -DestinationServer TheWildHearts -BackupFolder Z:\Backups -UseDefaultFilePaths -ContinueAfterDbccError + + The databases $Database on the server IronMaiden will be backed up the to the folder Z:\Backups. Then, an Agent job is created on server TheWildHearts with a Job Owner of THEBEARD\Rob to restore each database from that backup using the instance's default file paths. The database(s) is(are) then dropped on IronMaiden, the Agent job(s) run, a DBCC CHECKDB run on the restored database(s), and then the database(s) is(are) dropped. + + If there is a DBCC Error, the function will continue to perform rest of the actions and will create an Agent job with 'DBCCERROR' in the name and a Backup file with 'DBCCError' in the name. + #> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [parameter(Mandatory = $false)] + [DbaInstanceParameter]$Destination = $sqlinstance, + [PSCredential] + $DestinationCredential, + [parameter(Mandatory = $false)] + [Alias("NoCheck")] + [switch]$NoDbccCheckDb, + [parameter(Mandatory = $true)] + [string]$BackupFolder, + [parameter(Mandatory = $false)] + [string]$CategoryName = 'Rationalisation', + [parameter(Mandatory = $false)] + [string]$JobOwner, + [parameter(Mandatory = $false)] + [switch]$AllDatabases, + [ValidateSet("Default", "On", "Of")] + [string]$BackupCompression = 'Default', + [switch]$ReuseSourceFolderStructure, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + if (!$AllDatabases -and !$Database) { + Stop-Function -Message "You must specify at least one database. Use -Database or -AllDatabases." -ErrorRecord $_ + return + } + + $sourceserver = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlCredential -ParameterConnection + + if (-not $destination) { + $destination = $sqlinstance + $DestinationCredential = $SqlCredential + } + + if ($sqlinstance -ne $destination) { + + $destserver = Connect-SqlInstance -SqlInstance $destination -SqlCredential $DestinationCredential + + $sourcenb = $instance.ComputerName + $destnb = $instance.ComputerName + + if ($BackupFolder.StartsWith("\\") -eq $false -and $sourcenb -ne $destnb) { + Stop-Function -Message "Backup folder must be a network share if the source and destination servers are not the same." -ErrorRecord $_ -Target $backupFolder + return + } + } + else { + $destserver = $sourceserver + } + + $source = $sourceserver.DomainInstanceName + $destination = $destserver.DomainInstanceName + + if (!$jobowner) { + $jobowner = Get-SqlSaLogin $destserver + } + + if ($alldatabases -or !$Database) { + $database = ($sourceserver.databases | Where-Object { $_.IsSystemObject -eq $false -and ($_.Status -match 'Offline') -eq $false }).Name + } + + if (!(Test-DbaSqlPath -SqlInstance $destserver -Path $backupFolder)) { + $serviceaccount = $destserver.ServiceAccount + Stop-Function -Message "Can't access $backupFolder Please check if $serviceaccount has permissions." -ErrorRecord $_ -Target $backupFolder + } + + $jobname = "Rationalised Final Database Restore for $dbname" + $jobStepName = "Restore the $dbname database from Final Backup" + + if (!($destserver.Logins | Where-Object { $_.Name -eq $jobowner })) { + Stop-Function -Message "$destination does not contain the login $jobowner - Please fix and try again - Aborting." -ErrorRecord $_ -Target $jobowner + } + + function Start-SqlAgent { + <# + .SYNOPSIS + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param () + if ($destserver.VersionMajor -eq 8) { + $serviceName = 'MSSQLSERVER' + } + else { + $instance = $destserver.InstanceName + if ($instance.length -eq 0) { $instance = "MSSQLSERVER" } + $serviceName = "SQL Server Agent ($instance)" + } + + if ($Pscmdlet.ShouldProcess($destination, "Starting Sql Agent")) { + try { + $ipaddr = Resolve-SqlIpAddress $destserver + $agentservice = Get-Service -ComputerName $ipaddr -DisplayName $serviceName + + if ($agentservice.Status -ne 'Running') { + $agentservice.Start() + $timeout = New-Timespan -seconds 60 + $sw = [diagnostics.stopwatch]::StartNew() + $agentstatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status + while ($AgentStatus -ne 'Running' -and $sw.elapsed -lt $timeout) { + $agentStatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status + } + } + } + + catch { + throw $_ + } + + if ($agentservice.Status -ne 'Running') { + throw "Cannot start Agent Service on $destination - Aborting." + } + } + } + + function Start-DbccCheck { + <# + .SYNOPSIS + + #> + + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [object]$server, + [string]$dbname + ) + + $servername = $server.name + $db = $server.databases[$dbname] + + if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $servername")) { + try { + $null = $db.CheckTables('None') + Write-Message -Level Verbose -Message "DBCC CHECKDB finished successfully for $dbname on $servername." + } + + catch { + Write-Message -Level Warning -Message "DBCC CHECKDB failed." + Stop-Function -Message "Error occured: $_" -Target $agentservice -ErrorRecord $_ -Continue + + if ($force) { + return $true + } + else { + return $false + } + } + } + } + + function New-SqlAgentJobCategory { + <# + .SYNOPSIS + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ([string]$categoryname, + [object]$jobServer) + + if (!$jobServer.JobCategories[$categoryname]) { + if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $sourceserver")) { + try { + Write-Message -Level Verbose -Message "Creating Agent Job Category $categoryname." + $category = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobCategory + $category.Parent = $jobServer + $category.Name = $categoryname + $category.Create() + Write-Message -Level Verbose -Message "Created Agent Job Category $categoryname." + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job Category - $categoryname - Aborting." -Target $categoryname -ErrorRecord $_ + return + } + } + } + } + + function Restore-Database { + <# + .SYNOPSIS + Internal function. Restores .bak file to Sql database. Creates db if it doesn't exist. $filestructure is + a custom object that contains logical and physical file locations. + #> + + param ( + [Parameter(Mandatory = $true)] + [Alias('ServerInstance', 'SqlInstance', 'SqlServer')] + [object]$server, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$dbname, + [Parameter(Mandatory = $true)] + [string]$backupfile, + [string]$filetype = 'Database', + [Parameter(Mandatory = $true)] + [object]$filestructure, + [switch]$norecovery, + [PSCredential]$sqlCredential, + [switch]$TSql = $false + ) + + $server = Connect-SqlInstance -SqlInstance $server -SqlCredential $sqlCredential + $servername = $server.name + $server.ConnectionContext.StatementTimeout = 0 + $restore = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' + $restore.ReplaceDatabase = $true + + foreach ($file in $filestructure.values) { + $movefile = New-Object 'Microsoft.SqlServer.Management.Smo.RelocateFile' + $movefile.LogicalFileName = $file.logical + $movefile.PhysicalFileName = $file.physical + $null = $restore.RelocateFiles.Add($movefile) + } + + try { + if ($TSql) { + $restore.PercentCompleteNotification = 1 + $restore.add_Complete($complete) + $restore.ReplaceDatabase = $true + $restore.Database = $dbname + $restore.Action = $filetype + $restore.NoRecovery = $norecovery + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $device.name = $backupfile + $device.devicetype = 'File' + $restore.Devices.Add($device) + $restorescript = $restore.script($server) + return $restorescript + } + else { + $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $restore.add_PercentComplete($percent) + $restore.PercentCompleteNotification = 1 + $restore.add_Complete($complete) + $restore.ReplaceDatabase = $true + $restore.Database = $dbname + $restore.Action = $filetype + $restore.NoRecovery = $norecovery + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem + $device.name = $backupfile + $device.devicetype = 'File' + $restore.Devices.Add($device) + + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $restore.sqlrestore($server) + Write-Progress -id 1 -activity "Restoring $dbname to $servername" -status 'Complete' -Completed + + return $true + } + } + catch { + Stop-Function -Message "Restore failed" -ErrorRecord $_ -Target $dbname + return $false + } + } + + } + process { + if (Test-FunctionInterrupt) { + return + } + try { + Start-SqlAgent + } + catch { + Stop-Function -Message "Failure starting SQL Agent" -ErrorRecord $_ + return + } + + $start = Get-Date + Write-Message -Level Verbose -Message "Starting Rationalisation Script at $start." + + foreach ($dbname in $Database) { + + $db = $sourceserver.databases[$dbname] + + # The db check is needed when the number of databases exceeds 255, then it's no longer auto-populated + if (!$db) { + Stop-Function -Message "$dbname does not exist on $source. Aborting routine for this database." -Continue + } + + $lastFullBckDuration = (Get-DbaBackupHistory -SqlInstance $sourceserver -Database $dbname -LastFull).Duration + + if (-NOT ([string]::IsNullOrEmpty($lastFullBckDuration))) { + $lastFullBckDurationSec = $lastFullBckDuration.TotalSeconds + $lastFullBckDurationMin = [Math]::Round($lastFullBckDuration.TotalMinutes, 2) + + Write-Message -Level Verbose -Message "From the backup history the last full backup took $lastFullBckDurationSec seconds ($lastFullBckDurationMin minutes)" + if ($lastFullBckDurationSec -gt 600) { + Write-Message -Level Verbose -Message "Last full backup took more than 10 minutes. Do you want to continue?" + + # Set up the parts for the user choice + $Title = "Backup duration" + $Info = "Last full backup took more than $lastFullBckDurationMin minutes. Do you want to continue?" + + $Options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No (Skip)") + [int]$Defaultchoice = 0 + $choice = $host.UI.PromptForChoice($Title, $Info, $Options, $Defaultchoice) + # Check the given option + if ($choice -eq 1) { + Stop-Function -Message "You have chosen skipping the database $dbname because of last known backup time ($lastFullBckDurationMin minutes)." -ErrorRecord $_ -Target $dbname -Continue + Continue + } + } + } + else { + Write-Message -Level Verbose -Message "Couldn't find last full backup time for database $dbname using Get-DbaBackupHistory." + } + + $jobname = "Rationalised Database Restore Script for $dbname" + $jobStepName = "Restore the $dbname database from Final Backup" + $jobServer = $destserver.JobServer + + if ($jobServer.Jobs[$jobname].count -gt 0) { + if ($force -eq $false) { + Stop-Function -Message "FAILED: The Job $jobname already exists. Have you done this before? Rename the existing job and try again or use -Force to drop and recreate." -Continue + } + else { + if ($Pscmdlet.ShouldProcess($dbname, "Dropping $jobname on $source")) { + Write-Message -Level Verbose -Message "Dropping $jobname on $source." + $jobServer.Jobs[$jobname].Drop() + $jobServer.Jobs.Refresh() + } + } + } + + + Write-Message -Level Verbose -Message "Starting Rationalisation of $dbname." + ## if we want to Dbcc before to abort if we have a corrupt database to start with + if ($NoDbccCheckDb -eq $false) { + if ($Pscmdlet.ShouldProcess($dbname, "Running dbcc check on $dbname on $source")) { + Write-Message -Level Verbose -Message "Starting DBCC CHECKDB for $dbname on $source." + $dbccgood = Start-DbccCheck -Server $sourceserver -DBName $dbname + + if ($dbccgood -eq $false) { + if ($force -eq $false) { + Write-Message -Level Verbose -Message "DBCC failed for $dbname (you should check that). Aborting routine for this database." + continue + } + else { + Write-Message -Level Verbose -Message "DBCC failed, but Force specified. Continuing." + } + } + } + } + + if ($Pscmdlet.ShouldProcess($source, "Backing up $dbname")) { + Write-Message -Level Verbose -Message "Starting Backup for $dbname on $source." + ## Take a Backup + try { + $timenow = [DateTime]::Now.ToString('yyyyMMdd_HHmmss') + $backup = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Backup + $backup.Action = [Microsoft.SqlServer.Management.SMO.BackupActionType]::Database + $backup.BackupSetDescription = "Final Full Backup of $dbname Prior to Dropping" + $backup.Database = $dbname + $backup.Checksum = $True + if ($sourceserver.versionMajor -gt 9) { + $backup.CompressionOption = $BackupCompression + } + if ($force -and $dbccgood -eq $false) { + + $filename = "$backupFolder\$($dbname)_DBCCERROR_$timenow.bak" + } + else { + $filename = "$backupFolder\$($dbname)_Final_Before_Drop_$timenow.bak" + } + + $devicetype = [Microsoft.SqlServer.Management.Smo.DeviceType]::File + $backupDevice = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem($filename, $devicetype) + + $backup.Devices.Add($backupDevice) + #Progress + $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) + } + $backup.add_PercentComplete($percent) + $backup.add_Complete($complete) + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) + $backup.SqlBackup($sourceserver) + $null = $backup.Devices.Remove($backupDevice) + Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -status "Complete" -Completed + Write-Message -Level Verbose -Message "Backup Completed for $dbname on $source." + + Write-Message -Level Verbose -Message "Running Restore Verify only on Backup of $dbname on $source." + try { + $restoreverify = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' + $restoreverify.Database = $dbname + $restoreverify.Devices.AddDevice($filename, $devicetype) + $result = $restoreverify.SqlVerify($sourceserver) + + if ($result -eq $false) { + Write-Message -Level Warning -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database." + continue + } + + Write-Message -Level Verbose -Message "Restore Verify Only for $filename succeeded." + } + catch { + Stop-Function -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database. Exception: $_" -Target $filename -ErrorRecord $_ -Continue + } + } + catch { + Stop-Function -Message "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database. Exception: $_" -Target $filename -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Creating Automated Restore Job from Golden Backup for $dbname on $destination")) { + Write-Message -Level Verbose -Message "Creating Automated Restore Job from Golden Backup for $dbname on $destination." + try { + if ($force -eq $true -and $dbccgood -eq $false) { + $jobName = $jobname -replace "Rationalised", "DBCC ERROR" + } + + ## Create an agent job to restore the database + $job = New-Object Microsoft.SqlServer.Management.Smo.Agent.Job $jobServer, $jobname + $job.Name = $jobname + $job.OwnerLoginName = $jobowner + $job.Description = "This job will restore the $dbname database using the final backup located at $filename." + + ## Create a Job Category + if (!$jobServer.JobCategories[$categoryname]) { + New-SqlAgentJobCategory -JobServer $jobServer -categoryname $categoryname + } + + $job.Category = $categoryname + try { + if ($Pscmdlet.ShouldProcess($destination, "Creating Agent Job on $destination")) { + Write-Message -Level Verbose -Message "Created Agent Job $jobname on $destination." + $job.Create() + } + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job $jobname on $destination - aborting routine for this database." -Target $categoryname -ErrorRecord $_ -Continue + } + + ## Create Job Step + ## Aaron's Suggestion: In the restore script, add a comment block that tells the last known size of each file in the database. + ## Suggestion check for disk space before restore + ## Create Restore Script + try { + $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore + $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem $filename, 'FILE' + $restore.Devices.Add($device) + try { + $filelist = $restore.ReadFileList($destserver) + } + + catch { + throw 'File list could not be determined. This is likely due to connectivity issues or tiemouts with the Sql Server, the database version is incorrect, or the Sql Server service account does not have access to the file share. Script terminating.' + } + + $filestructure = Get-OfflineSqlFileStructure $destserver $dbname $filelist $ReuseSourceFolderStructure + + $jobStepCommand = Restore-Database $destserver $dbname $filename "Database" $filestructure -TSql -ErrorAction Stop + $jobStep = new-object Microsoft.SqlServer.Management.Smo.Agent.JobStep $job, $jobStepName + $jobStep.SubSystem = 'TransactSql' # 'PowerShell' + $jobStep.DatabaseName = 'master' + $jobStep.Command = $jobStepCommand + $jobStep.OnSuccessAction = 'QuitWithSuccess' + $jobStep.OnFailAction = 'QuitWithFailure' + if ($Pscmdlet.ShouldProcess($destination, "Creating Agent JobStep on $destination")) { + $null = $jobStep.Create() + } + $jobStartStepid = $jobStep.ID + Write-Message -Level Verbose -Message "Created Agent JobStep $jobStepName on $destination." + } + catch { + Stop-Function -Message "FAILED : To Create Agent JobStep $jobStepName on $destination - Aborting." -Target $jobStepName -ErrorRecord $_ -Continue + } + if ($Pscmdlet.ShouldProcess($destination, "Applying Agent Job $jobname to $destination")) { + $job.ApplyToTargetServer($destination) + $job.StartStepID = $jobStartStepid + $job.Alter() + } + } + catch { + Stop-Function -Message "FAILED : To Create Agent Job $jobname on $destination - aborting routine for $dbname. Exception: $_" -Target $jobname -ErrorRecord $_ -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Dropping Database $dbname on $sourceserver")) { + ## Drop the database + try { + $null = Remove-DbaDatabase -SqlInstance $sourceserver -Database $dbname -Confirm:$false + Write-Message -Level Verbose -Message "Dropped $dbname Database on $source prior to running the Agent Job" + } + catch { + Stop-Function -Message "FAILED : To Drop database $dbname on $server - aborting routine for $dbname. Exception: $_" -Continue + } + } + + if ($Pscmdlet.ShouldProcess($destination, "Running Agent Job on $destination to restore $dbname")) { + ## Run the restore job to restore it + Write-Message -Level Verbose -Message "Starting $jobname on $destination." + try { + $job = $destserver.JobServer.Jobs[$jobname] + $job.Start() + $job.Refresh() + $status = $job.CurrentRunStatus + + while ($status -ne 'Idle') { + Write-Message -Level Verbose -Message "Restore Job for $dbname on $destination is $status." + Start-Sleep -Seconds 15 + $job.Refresh() + $status = $job.CurrentRunStatus + } + + Write-Message -Level Verbose -Message "Restore Job $jobname has completed on $destination." + Write-Message -Level Verbose -Message "Sleeping for a few seconds to ensure the next step (DBCC) succeeds." + Start-Sleep -Seconds 10 ## This is required to ensure the next DBCC Check succeeds + } + catch { + Stop-Function -Message "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname. Exception: $_" -Continue + } + + if ($job.LastRunOutcome -ne 'Succeeded') { + # LOL, love the plug. + Write-Message -Level Warning -Message "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname." + Write-Message -Level Warning -Message "Check the Agent Job History on $destination - if you have SSMS2016 July release or later." + Write-Message -Level Warning -Message "Get-SqlAgentJobHistory -JobName '$jobname' -ServerInstance $destination -OutcomesType Failed." + continue + } + } + + $refreshRetries = 1 + + while ($null -eq ($destserver.databases[$dbname]) -and $refreshRetries -lt 6) { + Write-Message -Level verbose -Message "Database $dbname not found! Refreshing collection." + + #refresh database list, otherwise the next step (DBCC) can fail + $destserver.Databases.Refresh() + + Start-Sleep -Seconds 1 + + $refreshRetries += 1 + } + + + ## Run a Dbcc No choice here + if ($Pscmdlet.ShouldProcess($dbname, "Running Dbcc CHECKDB on $dbname on $destination")) { + Write-Message -Level Verbose -Message "Starting Dbcc CHECKDB for $dbname on $destination." + $null = Start-DbccCheck -Server $destserver -DbName $dbname + } + + if ($Pscmdlet.ShouldProcess($dbname, "Dropping Database $dbname on $destination")) { + ## Drop the database + try { + $null = Remove-DbaDatabase -SqlInstance $sourceserver -Database $dbname -Confirm:$false + Write-Message -Level Verbose -Message "Dropped $dbname database on $destination." + } + catch { + Stop-Function -Message "FAILED : To Drop database $dbname on $destination - Aborting. Exception: $_" -Target $dbname -ErrorRecord $_ -Continue + } + } + Write-Message -Level Verbose -Message "Rationalisation Finished for $dbname." + + [PSCustomObject]@{ + SqlInstance = $source + DatabaseName = $dbname + JobName = $jobname + TestingInstance = $destination + BackupFolder = $backupFolder + } + } + } + + end { + if (Test-FunctionInterrupt) { + return + } + if ($Pscmdlet.ShouldProcess("console", "Showing final message")) { + $End = Get-Date + Write-Message -Level Verbose -Message "Finished at $End." + $Duration = $End - $start + Write-Message -Level Verbose -Message "Script Duration: $Duration." + } + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Remove-SqlDatabaseSafely + } +} diff --git a/functions/Remove-DbaDatabaseSnapshot.ps1 b/functions/Remove-DbaDatabaseSnapshot.ps1 deleted file mode 100644 index 00d4828f2dba..000000000000 --- a/functions/Remove-DbaDatabaseSnapshot.ps1 +++ /dev/null @@ -1,195 +0,0 @@ -Function Remove-DbaDatabaseSnapshot -{ -<# -.SYNOPSIS -Removes database snapshots - -.DESCRIPTION -Removes (drops) database snapshots from the server - -.PARAMETER SqlInstance -The SQL Server that you're connecting to - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Snapshots -Removes snapshot databases with this names only - -.PARAMETER Databases -Removes snapshots for only specific base dbs - -.PARAMETER Snapshots -Removes specific snapshots - -.PARAMETER AllSnapshots -Specifies that you want to remove all snapshots from the server - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER PipelineSnapshot -Internal parameter - -.NOTES -Tags: Snapshot -Author: niphlod - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK - https://dbatools.io/Remove-DbaDatabaseSnapshot - -.EXAMPLE -Remove-DbaDatabaseSnapshot -SqlServer sqlserver2014a - -Removes all database snapshots from sqlserver2014a - -.EXAMPLE -Remove-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Snapshots HR_snap_20161201, HR_snap_20161101 - -Removes database snapshots named HR_snap_20161201 and HR_snap_20161101 - -.EXAMPLE -Remove-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Databases HR, Accounting - -Removes all database snapshots having HR and Accounting as base dbs - -.EXAMPLE -Remove-DbaDatabaseSnapshot -SqlServer sqlserver2014a -Snapshots HR_snapshot, Accounting_snapshot - -Removes HR_snapshot and Accounting_snapshot - - -.EXAMPLE -Get-DbaDatabaseSnapshot -SqlServer sql2016 | Where SnapshotOf -like '*dumpsterfire*' | Remove-DbaDatabaseSnapshot - -Removes all snapshots associated with databases that have dumpsterfire in the name - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [string[]]$SqlInstance, - [PsCredential]$Credential, - [parameter(ValueFromPipeline = $true)] - [object]$PipelineSnapshot, - [switch]$AllSnapshots - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlSnapshotsAndDatabases -SqlServer $SqlInstance[0] -SqlCredential $Credential - } - } - - BEGIN - { - $databases = $psboundparameters.Databases - $snapshots = $psboundparameters.Snapshots - } - - PROCESS - { - if ($snapshots.count -eq 0 -and $databases.count -eq 0 -and $AllSnapshots -eq $false -and $PipelineSnapshot -eq $null) - { - Write-Warning "You must specify -Snapshots, -Databases or -AllSnapshots" - return - } - - # handle the database object passed by the pipeline - if ($PipelineSnapshot.PSTypeNames -eq 'dbatools.customobject') - { - If ($Pscmdlet.ShouldProcess($PipelineSnapshot.SnapshotDb.Parent.name, "Remove db snapshot '$($PipelineSnapshot.SnapshotDb.Name)'")) - { - $dropped = Remove-SqlDatabase -SqlServer $PipelineSnapshot.SnapshotDb.Parent -DBName $PipelineSnapshot.SnapshotDb.Name - - if ($dropped -match "Success") - { - $status = "Dropped" - } - else - { - Write-Warning $dropped - $status = "Drop failed" - } - - [PSCustomObject]@{ - Server = $PipelineSnapshot.Server - Database = $PipelineSnapshot.Database - SnapshotOf = $PipelineSnapshot.SnapshotOf - Status = $status - } - } - return - } - - # if piped value either doesn't exist or is not the proper type - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - - } - catch - { - Write-Warning "Can't connect to $instance" - Continue - } - - $dbs = $server.Databases - - if ($databases.count -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.DatabaseSnapshotBaseName } - } - - if ($snapshots.count -gt 0) - { - $dbs = $dbs | Where-Object { $snapshots -contains $_.Name } - } - - if ($snapshots.count -eq 0 -and $databases.count -eq 0) - { - $dbs = $dbs | Where-Object IsDatabaseSnapshot -eq $true | Sort-Object DatabaseSnapshotBaseName, Name - } - - foreach ($db in $dbs) - { - If ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $($db.Name)")) - { - $dropped = Remove-SqlDatabase -SqlServer $server -DBName $db.Name -SqlCredential $Credential - - if ($dropped -match "Success") - { - $status = "Dropped" - } - else - { - Write-Warning $dropped - $status = "Drop failed" - } - - [PSCustomObject]@{ - Server = $server.name - Database = $db.Name - SnapshotOf = $db.DatabaseSnapshotBaseName - Status = $status - } - } - } - } - } -} diff --git a/functions/Remove-DbaDbCertificate.ps1 b/functions/Remove-DbaDbCertificate.ps1 new file mode 100644 index 000000000000..4b082a2f0f64 --- /dev/null +++ b/functions/Remove-DbaDbCertificate.ps1 @@ -0,0 +1,139 @@ +function Remove-DbaDbCertificate { + <# +.SYNOPSIS +Deletes specified database certificate + +.DESCRIPTION +Deletes specified database certificate + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER SqlCredential +Allows you to login to SQL Server using alternative credentials. + +.PARAMETER Database +The database where the certificate will be removed. + +.PARAMETER Certificate +The certificate that will be removed + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER InputObject +Piped certificate objects + +.NOTES +Tags: Certificate +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Remove-DbaDbCertificate -SqlInstance Server1 + +The certificate in the master database on server1 will be removed if it exists. + +.EXAMPLE +Remove-DbaDbCertificate -SqlInstance Server1 -Database db1 -Confirm:$false + +Suppresses all prompts to remove the certificate in the 'db1' database and drops the key. + + +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [object[]]$Database, + [parameter(Mandatory, ParameterSetName = "instance")] + [object[]]$Certificate, + [parameter(ValueFromPipeline, ParameterSetName = "collection")] + [Microsoft.SqlServer.Management.Smo.Certificate[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Remove-DbaDatabaseCertificate + + function drop-cert ($smocert) { + $server = $smocert.Parent.Parent + $instance = $server.DomainInstanceName + $cert = $smocert.Name + $db = $smocert.Parent.Name + + $output = [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $instance + Database = $db + Certificate = $cert + Status = $null + } + + if ($Pscmdlet.ShouldProcess($instance, "Dropping the certificate named $cert for database '$db' on $server")) { + try { + $smocert.Drop() + Write-Message -Level Verbose -Message "Successfully removed certificate named $cert from the $db database on $server" + $output.status = "Success" + } + catch { + $output.Status = "Failure" + Stop-Function -Message "Failed to drop certificate named $cert from $db on $server." -Target $smocert -InnerErrorRecord $_ -Continue + } + $output + } + } + } + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($db in $Database) { + $currentdb = $server.Databases[$db] + + if ($null -eq $currentdb) { + Stop-Function -Message "Database '$db' does not exist on $server" -Target $currentdb -Continue + } + + if (-not $currentdb.IsAccessible) { + Stop-Function -Message "Database '$db' is not accessible" -Target $currentdb -Continue + } + + foreach ($cert in $certificate) { + $smocert = $currentdb.Certificates[$cert] + + if ($null -eq $smocert) { + Stop-Function -Message "No certificate named $cert exists in the $db database on $server" -Target $currentdb.Certificates -Continue + } + + Drop-Cert -smocert $smocert + } + } + } + + foreach ($smocert in $InputObject) { + Drop-Cert -smocert $smocert + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaDbSnapshot.ps1 b/functions/Remove-DbaDbSnapshot.ps1 new file mode 100644 index 000000000000..8b8275f56acf --- /dev/null +++ b/functions/Remove-DbaDbSnapshot.ps1 @@ -0,0 +1,213 @@ +#ValidationTags#FlowControl# +function Remove-DbaDbSnapshot { + <# + .SYNOPSIS + Removes database snapshots + + .DESCRIPTION + Removes (drops) database snapshots from the server + + .PARAMETER SqlInstance + The SQL Server that you're connecting to + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + Removes snapshots for only this specific base db + + .PARAMETER ExcludeDatabase + Removes snapshots excluding this specific base dbs + + .PARAMETER Snapshot + Restores databases from snapshot with this name only + + .PARAMETER AllSnapshots + Specifies that you want to remove all snapshots from the server + + .PARAMETER Force + Will forcibly kill all running queries that prevent the drop process. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER PipelineSnapshot + Internal parameter + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDbSnapshot + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a + + Removes all database snapshots from sqlserver2014a + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snap_20161201, HR_snap_20161101 + + Removes database snapshots named HR_snap_20161201 and HR_snap_20161101 + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Removes all database snapshots having HR and Accounting as base dbs + + .EXAMPLE + Remove-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snapshot, Accounting_snapshot + + Removes HR_snapshot and Accounting_snapshot + + .EXAMPLE + Get-DbaDbSnapshot -SqlInstance sql2016 | Where SnapshotOf -like '*dumpsterfire*' | Remove-DbaDbSnapshot + + Removes all snapshots associated with databases that have dumpsterfire in the name + +#> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Snapshot, + [parameter(ValueFromPipeline = $true)] + [object]$PipelineSnapshot, + [switch]$AllSnapshots, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + if (!$Snapshot -and !$Database -and !$AllSnapshots -and $null -eq $PipelineSnapshot -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify -Snapshot, -Database, -Exclude or -AllSnapshots" + return + } + # handle the database object passed by the pipeline + # do we need a specialized type back ? + if ($null -ne $PipelineSnapshot -and $PipelineSnapshot.getType().Name -eq 'pscustomobject') { + if ($Pscmdlet.ShouldProcess($PipelineSnapshot.SnapshotDb.Parent.DomainInstanceName, "Remove db snapshot $($PipelineSnapshot.SnapshotDb.Name)")) { + try { + $server = $PipelineSnapshot.SnapshotDb.Parent + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + try { + if ($Force) { + $server.KillAllProcesses($PipelineSnapshot.SnapshotDb.Name) + } + $null = $server.Query("drop database [$($PipelineSnapshot.SnapshotDb.Name)]") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $status = "Drop failed" + } + + [PSCustomObject]@{ + ComputerName = $PipelineSnapshot.SnapshotDb.Parent.NetName + InstanceName = $PipelineSnapshot.SnapshotDb.Parent.ServiceName + SqlInstance = $PipelineSnapshot.SnapshotDb.Parent.DomainInstanceName + Database = $PipelineSnapshot.Database + SnapshotOf = $PipelineSnapshot.SnapshotOf + Status = $status + } + } + return + } + + # if piped value either doesn't exist or is not the proper type + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + + $alldbq = @" +SELECT sn.name AS Name, dt.name AS DatabaseSnapshotBaseName, sn.create_date AS CreateDate, +CASE WHEN sn.state = 6 THEN 0 ELSE 1 END AS IsAccessible, +CASE WHEN sn.source_database_id IS NOT NULL THEN 1 ELSE 0 END AS IsDatabaseSnapshot +FROM sys.databases sn +LEFT JOIN sys.databases dt +ON sn.source_database_id = dt.database_id +"@ + $dbs = $server.Query($alldbq) + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.DatabaseSnapshotBaseName } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.DatabaseSnapshotBaseName } + } + + if ($Snapshot) { + $dbs = $dbs | Where-Object { $Snapshot -contains $_.Name } + } + + if (!$Snapshot -and !$Database) { + $dbs = $dbs | Where-Object IsDatabaseSnapshot -eq $true | Sort-Object DatabaseSnapshotBaseName, Name + } + + + foreach ($dbraw in $dbs) { + if ($dbraw.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "Database $dbraw is not accessible." + continue + } + $db = $server.Databases[$($dbraw.Name)] + if ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $db")) { + $basedb = $db.DatabaseSnapshotBaseName + try { + if ($Force) { + # cannot drop the snapshot if someone is using it + $server.KillAllProcesses($db) + } + $null = $server.Query("drop database $db") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $status = "Drop failed" + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + SnapshotOf = $basedb + Status = $status + } + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Remove-DbaDatabaseSnapshot + } +} \ No newline at end of file diff --git a/functions/Remove-DbaDbUser.ps1 b/functions/Remove-DbaDbUser.ps1 new file mode 100644 index 000000000000..00e79ee87594 --- /dev/null +++ b/functions/Remove-DbaDbUser.ps1 @@ -0,0 +1,240 @@ +function Remove-DbaDbUser { + <# + .SYNOPSIS + Drop database user + + .DESCRIPTION + If user is the owner of a schema with the same name and if if the schema does not have any underlying objects the schema will be + dropped. If user owns more than one schema, the owner of the schemas that does not have the same name as the user, will be + changed to 'dbo'. If schemas have underlying objects, you must specify the -Force parameter so the user can be dropped. + + .PARAMETER SqlInstance + The SQL Instances that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER User + Specifies the list of users to remove. + + .PARAMETER InputObject + Support piping from Get-DbaDatabaseUser. + + .PARAMETER Force + If enabled this will force the change of the owner to 'dbo' for any schema which owner is the User. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, User, Login, Security + Author: Doug Meyers (@dgmyrs) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaDbUser + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -User user1 + + Drops user1 from all databases it exists in on server 'sqlserver2014'. + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -Database database1 -User user1 + + Drops user1 from the database1 database on server 'sqlserver2014'. + + .EXAMPLE + Remove-DbaDbUser -SqlInstance sqlserver2014 -ExcludeDatabase model -User user1 + + Drops user1 from all databases it exists in on server 'sqlserver2014' except for the model database. + + .EXAMPLE + Get-DbaDatabaseUser sqlserver2014 | Where-Object Name -In "user1" | Remove-DbaDbUser + + Drops user1 from all databases it exists in on server 'sqlserver2014'. + +#> + + [CmdletBinding(DefaultParameterSetName = 'User', SupportsShouldProcess = $true)] + Param ( + [parameter(Position = 1, Mandatory, ValueFromPipeline, ParameterSetName = 'User')] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + + [parameter(ParameterSetName = 'User')] + [Alias("Credential")] + [PSCredential] + $SqlCredential, + + [parameter(ParameterSetName = 'User')] + [Alias("Databases")] + [object[]]$Database, + + [parameter(ParameterSetName = 'User')] + [object[]]$ExcludeDatabase, + + [parameter(Mandatory, ParameterSetName = 'User')] + [object[]]$User, + + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')] + [Microsoft.SqlServer.Management.Smo.User[]]$InputObject, + + [parameter(ParameterSetName = 'User')] + [parameter(ParameterSetName = 'Object')] + [switch]$Force, + + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + function Remove-DbUser { + [CmdletBinding()] + param ([Microsoft.SqlServer.Management.Smo.User[]]$users) + + foreach ($user in $users) { + $db = $user.Parent + $server = $db.Parent + $ownedObjects = $false + Write-Message -Level Verbose -Message "Removing User $user from Database $db on target $server" + + # Drop Schemas owned by the user before droping the user + $schemaUrns = $user.EnumOwnedObjects() | Where-Object Type -EQ Schema + if ($schemaUrns) { + Write-Message -Level Verbose -Message "User $user owns $($schemaUrns.Count) schema(s)." + + # Need to gather up the schema changes so they can be done in a non-desctructive order + $alterSchemas = @() + $dropSchemas = @() + + foreach ($schemaUrn in $schemaUrns) { + $schema = $server.GetSmoObject($schemaUrn) + + # Drop any schema that is the same name as the user + if ($schema.Name -EQ $user.Name) { + # Check for owned objects early so we can exit before any changes are made + $ownedUrns = $schema.EnumOwnedObjects() + if (-Not $ownedUrns) { + $dropSchemas += $schema + } + else { + Write-Message -Level Warning -Message "User owns objects in the database and will not be removed." + foreach ($ownedUrn in $ownedUrns) { + $obj = $server.GetSmoObject($ownedUrn) + Write-Message -Level Warning -Message "User $user owns $($obj.GetType().Name) $obj" + } + $ownedObjects = $true + } + } + + # Change the owner of any schema not the same name as the user + if ($schema.Name -NE $user.Name) { + # Check for owned objects early so we can exit before any changes are made + $ownedUrns = $schema.EnumOwnedObjects() + if (($ownedUrns -And $Force) -Or (-Not $ownedUrns)) { + $alterSchemas += $schema + } + else { + Write-Message -Level Warning -Message "User $user owns the Schema $schema, which owns $($ownedUrns.Count) Object(s). If you want to change the schemas' owner to [dbo] and drop the user anyway, use -Force parameter. User $user will not be removed." + $ownedObjects = $true + } + } + } + } + + if (-Not $ownedObjects) { + try { + # Alter Schemas + foreach ($schema in $alterSchemas) { + Write-Message -Level Verbose -Message "Owner of Schema $schema will be changed to [dbo]." + if ($PSCmdlet.ShouldProcess($server, "Change the owner of Schema $schema to [dbo].")) { + $schema.Owner = "dbo" + $schema.Alter() + } + } + + # Drop Schemas + foreach ($schema in $dropSchemas) { + if ($PSCmdlet.ShouldProcess($server, "Drop Schema $schema from Database $db.")) { + $schema.Drop() + } + } + + # Finally, Drop user + if ($PSCmdlet.ShouldProcess($server, "Drop User $user from Database $db.")) { + $user.Drop() + } + + $status = "Dropped" + + } + catch { + Write-Error -Message "Could not drop $user from Database $db on target $server" + $status = "Not Dropped" + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + User = $user + Status = $status + } + } + } + } + } + + process { + if ($InputObject) { + Remove-DbUser $InputObject + } + else { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $databases = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + foreach ($db in $databases) { + Write-Message -Level Verbose -Message "Get users in Database $db on target $server" + $users = Get-DbaDatabaseUser -SqlInstance $server -Database $db.Name + $users = $users | Where-Object Name -In $User + Remove-DbUser $users + } + } + } + } + +} \ No newline at end of file diff --git a/functions/Remove-DbaLogin.ps1 b/functions/Remove-DbaLogin.ps1 new file mode 100644 index 000000000000..bf2220396238 --- /dev/null +++ b/functions/Remove-DbaLogin.ps1 @@ -0,0 +1,126 @@ +function Remove-DbaLogin { + <# +.SYNOPSIS +Drops a Login + +.DESCRIPTION +Tries a bunch of different ways to remove a Login or two or more. + +.PARAMETER SqlInstance +The SQL Server instance holding the Logins to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. + +.PARAMETER SqlCredential +Allows you to login to servers using alternative credentials. + +.PARAMETER Login +The Login(s) to process - this list is auto-populated from the server. If unspecified, all Logins will be processed. + +.PARAMETER InputObject +A collection of Logins (such as returned by Get-DbaLogin), to be removed. + +.PARAMETER Force +Kills any sessions associated with the login prior to drop + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Delete, Logins + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Remove-DbaLogin + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin + +Prompts then removes the Login mylogin on SQL Server sql2016 + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin, yourlogin + +Prompts then removes the Logins mylogin and yourlogin on SQL Server sql2016 + +.EXAMPLE +Remove-DbaLogin -SqlInstance sql2016 -Login mylogin -Confirm:$false + +Does not prompt and swiftly removes mylogin on SQL Server sql2016 + +.EXAMPLE +Get-DbaLogin -SqlInstance server\instance -Login yourlogin | Remove-DbaLogin + +removes mylogin on SQL Server server\instance + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = "Default")] + Param ( + [parameter(Mandatory, ParameterSetName = "instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(Mandatory = $false)] + [Alias("Credential")] + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = "instance")] + [string[]]$Login, + [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = "Logins")] + [Microsoft.SqlServer.Management.Smo.Login[]]$InputObject, + [switch]$Force, + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $InputObject += $server.Logins | Where-Object { $_.Name -in $Login } + } + + foreach ($currentlogin in $InputObject) { + try { + $server = $currentlogin.Parent + if ($Pscmdlet.ShouldProcess("$currentlogin on $server", "KillLogin")) { + if ($force) { + $null = Stop-DbaProcess -SqlInstance $server -Login $currentlogin + } + + $currentlogin.Drop() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Login = $currentlogin.name + Status = "Dropped" + } + } + } + catch { + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Login = $currentlogin.name + Status = $_ + } + Stop-Function -Message "Could not drop Login $currentlogin on $server" -ErrorRecord $_ -Target $currentlogin -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaNetworkCertificate.ps1 b/functions/Remove-DbaNetworkCertificate.ps1 new file mode 100644 index 000000000000..309ee180fe03 --- /dev/null +++ b/functions/Remove-DbaNetworkCertificate.ps1 @@ -0,0 +1,139 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Remove-DbaNetworkCertificate { + <# + .SYNOPSIS + Removes the network certificate for SQL Server instance + + .DESCRIPTION + Removes the network certificate for SQL Server instance. This setting is found in Configuration Manager. + + .PARAMETER SqlInstance + The target SQL Server - defaults to localhost. If target is a cluster, you must also specify InstanceClusterName (see below) + + .PARAMETER Credential + Allows you to login to the computer (not sql instance) using alternative credentials. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + Remove-DbaNetworkCertificate + + Removes the Network Certificate for the default instance (MSSQLSERVER) on localhost + + .EXAMPLE + Remove-DbaNetworkCertificate -SqlInstance sql1\SQL2008R2SP2 + + Removes the Network Certificate for the SQL2008R2SP2 instance on sql1 + + .EXAMPLE + Remove-DbaNetworkCertificate -SqlInstance localhost\SQL2008R2SP2 -WhatIf + + Shows what would happen if the command were run + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low", DefaultParameterSetName = 'Default')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [PSCredential] + + $Credential, + + [switch] + [Alias('Silent')]$EnableException + ) + process { + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname" + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance" -Target $instance -Continue -Category InvalidArgument + } + + Write-Message -Level Output -Message "Connecting to SQL WMI on $($instance.ComputerName)" + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($($instance.InstanceName))" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance" -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$($args[0])\MSSQLServer\SuperSocketNetLib" + $cert = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + Set-ItemProperty -Path $regpath -Name Certificate -Value $null + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + RemovedThumbprint = $cert.Thumbprint + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $ComputerName to remove the cert")) { + try { + Invoke-Command2 -ComputerName $resolved.fqdn -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.fqdn) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaOrphanUser.ps1 b/functions/Remove-DbaOrphanUser.ps1 new file mode 100644 index 000000000000..b94cdb2b54f6 --- /dev/null +++ b/functions/Remove-DbaOrphanUser.ps1 @@ -0,0 +1,333 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Remove-DbaOrphanUser { + <# + .SYNOPSIS + Drop orphan users with no existing login to map + + .DESCRIPTION + An orphan user is defined by a user that does not have their matching login. (Login property = ""). + + If user is the owner of the schema with the same name and if if the schema does not have any underlying objects the schema will be dropped. + + If user owns more than one schema, the owner of the schemas that does not have the same name as the user, will be changed to 'dbo'. If schemas have underlying objects, you must specify the -Force parameter so the user can be dropped. + + If exists a login to map the drop will not be performed unless you specify the -Force parameter (only when calling from Repair-DbaOrphanUser. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER User + Specifies the list of users to remove. + + .PARAMETER Force + If this switch is enabled: + If exists any schema which owner is the User, this will force the change of the owner to 'dbo'. + If exists a login to map the drop will not be performed unless you specify this parameter. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Orphan, Database, Security, Login + Author: Claudio Silva (@ClaudioESSilva) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaOrphanUser + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sql2005 + + Finds and drops all orphan users without matching Logins in all databases present on server 'sql2005'. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -SqlCredential $cred + + Finds and drops all orphan users without matching Logins in all databases present on server 'sqlserver2014a'. SQL Server authentication will be used in connecting to the server. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1, db2 -Force + + Finds and drops orphan users even if they have a matching Login on both db1 and db2 databases. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -ExcludeDatabase db1, db2 -Force + + Finds and drops orphan users even if they have a matching Login from all databases except db1 and db2. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -User OrphanUser + + Removes user OrphanUser from all databases only if there is no matching login. + + .EXAMPLE + Remove-DbaOrphanUser -SqlInstance sqlserver2014a -User OrphanUser -Force + + Removes user OrphanUser from all databases even if they have a matching Login. Any schema that the user owns will change ownership to dbo. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [object[]]$User, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($Instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Attempting to connect to $Instance." + try { + $server = Connect-SqlInstance -SqlInstance $Instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Can't connect to $Instance or access denied. Skipping." + continue + } + + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + $CallStack = Get-PSCallStack | Select-Object -Property * + if ($CallStack.Count -eq 1) { + $StackSource = $CallStack[0].Command + } + else { + #-2 because index base is 0 and we want the one before the last (the last is the actual command) + $StackSource = $CallStack[($CallStack.Count - 2)].Command + } + + if ($DatabaseCollection) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + + if ($StackSource -eq "Repair-DbaOrphanUser") { + Write-Message -Level Verbose -Message "Call origin: Repair-DbaOrphanUser." + #Will use collection from parameter ($User) + } + else { + Write-Message -Level Verbose -Message "Validating users on database $db." + + if ($User.Count -eq 0) { + #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $User = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + } + else { + + #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $User = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($User -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + + } + } + + if ($User.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found." + foreach ($dbuser in $User) { + $SkipUser = $false + + $ExistLogin = $null + + if ($StackSource -ne "Repair-DbaOrphanUser") { + #Need to validate Existing Login because the call does not came from Repair-DbaOrphanUser + $ExistLogin = $server.logins | Where-Object { + $_.Isdisabled -eq $False -and + $_.IsSystemObject -eq $False -and + $_.IsLocked -eq $False -and + $_.Name -eq $dbuser.Name + } + } + + #Schemas only appears on SQL Server 2005 (v9.0) + if ($server.versionMajor -gt 8) { + + #reset variables + $AlterSchemaOwner = "" + $DropSchema = "" + + #Validate if user owns any schema + $Schemas = @() + $Schemas = $db.Schemas | Where-Object Owner -eq $dbuser.Name + + if (@($Schemas).Count -gt 0) { + Write-Message -Level Verbose -Message "User $dbuser owns one or more schemas." + + foreach ($sch in $Schemas) { + <# + On sql server 2008 or lower the EnumObjects method does not accept empty parameter. + 0x1FFFFFFF is the way we can say we want everything known by those versions + + When it is an higher version we can use empty to get all + #> + if ($server.versionMajor -lt 11) { + $NumberObjects = ($db.EnumObjects(0x1FFFFFFF) | Where-Object { $_.Schema -eq $sch.Name } | Measure-Object).Count + } + else { + $NumberObjects = ($db.EnumObjects() | Where-Object { $_.Schema -eq $sch.Name } | Measure-Object).Count + } + + if ($NumberObjects -gt 0) { + if ($Force) { + Write-Message -Level Verbose -Message "Parameter -Force was used! The schema '$($sch.Name)' have $NumberObjects underlying objects. We will change schema owner to 'dbo' and drop the user." + + if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'. -Force used.")) { + $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "ALTER OWNER" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "dbo" + } + } + } + else { + Write-Message -Level Warning -Message "Schema '$($sch.Name)' owned by user $($dbuser.Name) have $NumberObjects underlying objects. If you want to change the schemas' owner to 'dbo' and drop the user anyway, use -Force parameter. Skipping user '$dbuser'." + $SkipUser = $true + break + } + } + else { + if ($sch.Name -eq $dbuser.Name) { + Write-Message -Level Verbose -Message "The schema '$($sch.Name)' have the same name as user $dbuser. Schema will be dropped." + + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping schema '$($sch.Name)'.")) { + $DropSchema += "DROP SCHEMA [$($sch.Name)]" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "DROP" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "N/A" + } + } + } + else { + Write-Message -Level Warning -Message "Schema '$($sch.Name)' does not have any underlying object. Ownership will be changed to 'dbo' so the user can be dropped. Remember to re-check permissions on this schema!" + + if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'.")) { + $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + SchemaName = $sch.Name + Action = "ALTER OWNER" + SchemaOwnerBefore = $sch.Owner + SchemaOwnerAfter = "dbo" + } + } + } + } + } + + } + else { + Write-Message -Level Verbose -Message "User $dbuser does not own any schema. Will be dropped." + } + + $query = "$AlterSchemaOwner `r`n$DropSchema `r`nDROP USER " + $dbuser + + Write-Message -Level Debug -Message $query + } + else { + $query = "EXEC master.dbo.sp_droplogin @loginame = N'$($dbuser.name)'" + } + + if ($ExistLogin) { + if (-not $SkipUser) { + if ($Force) { + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user $dbuser using -Force")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User $dbuser was dropped from $($db.Name). -Force parameter was used!" + } + } + else { + Write-Message -Level Warning -Message "Orphan user $($dbuser.Name) has a matching login. The user will not be dropped. If you want to drop anyway, use -Force parameter." + Continue + } + } + } + else { + if (-not $SkipUser) { + if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user $dbuser")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User $dbuser was dropped from $($db.Name)." + } + } + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database $db." + } + #reset collection + $User = $null + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $db -Continue + } + } + } + else { + Write-Message -Level Verbose -Message "There are no databases to analyse." + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Remove-SqlOrphanUser + } +} diff --git a/functions/Remove-DbaPfDataCollectorCounter.ps1 b/functions/Remove-DbaPfDataCollectorCounter.ps1 new file mode 100644 index 000000000000..867467c5b973 --- /dev/null +++ b/functions/Remove-DbaPfDataCollectorCounter.ps1 @@ -0,0 +1,137 @@ +function Remove-DbaPfDataCollectorCounter { + <# + .SYNOPSIS + Removes a Performance Data Collector Counter. + + .DESCRIPTION + Removes a Performance Data Collector Counter. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The name of the Collector Set to search. + + .PARAMETER Collector + The name of the Collector to remove. + + .PARAMETER Counter + The name of the Counter - in the form of '\Processor(_Total)\% Processor Time'. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaPfDataCollectorCounter + + .EXAMPLE + Remove-DbaPfDataCollectorCounter -ComputerName sql2017 -CollectorSet 'System Correlation' -Collector DataCollector01 -Counter '\LogicalDisk(*)\Avg. Disk Queue Length' + + Prompts for confirmation then removes the '\LogicalDisk(*)\Avg. Disk Queue Length' counter within the DataCollector01 collector within the System Correlation collector set on sql2017. + + .EXAMPLE + Get-DbaPfDataCollectorCounter | Out-GridView -PassThru | Remove-DbaPfDataCollectorCounter -Confirm:$false + + Allows you to select which counters you'd like on localhost and does not prompt for confirmation. + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param ( + [DbaInstance[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [Alias("DataCollector")] + [string[]]$Collector, + [parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("Name")] + [object[]]$Counter, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $setscript = { + $setname = $args[0]; $removexml = $args[1] + $CollectorSet = New-Object -ComObject Pla.DataCollectorSet + $CollectorSet.SetXml($removexml) + $CollectorSet.Commit($setname, $null, 0x0003) #add or modify. + $CollectorSet.Query($setname, $Null) + } + } + process { + if ($InputObject.Credential -and (Test-Bound -ParameterName Credential -Not)) { + $Credential = $InputObject.Credential + } + + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorCounter -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet -Collector $Collector -Counter $Counter + } + } + + if ($InputObject) { + if (-not $InputObject.CounterObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorCounter." + return + } + } + + foreach ($object in $InputObject) { + $computer = $InputObject.ComputerName + $null = Test-ElevationRequirement -ComputerName $computer -Continue + $setname = $InputObject.DataCollectorSet + $collectorname = $InputObject.DataCollector + + $xml = [xml]($InputObject.DataCollectorSetXml) + + foreach ($countername in $counter) { + $node = $xml.SelectSingleNode("//Name[.='$collectorname']").SelectSingleNode("//Counter[.='$countername']") + $null = $node.ParentNode.RemoveChild($node) + $node = $xml.SelectSingleNode("//Name[.='$collectorname']").SelectSingleNode("//CounterDisplayName[.='$countername']") + $null = $node.ParentNode.RemoveChild($node) + } + + $plainxml = $xml.OuterXml + + if ($Pscmdlet.ShouldProcess("$computer", "Remove $countername from $collectorname with the $setname collection set")) { + try { + $results = Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $setname, $plainxml -ErrorAction Stop -Raw + Write-Message -Level Verbose -Message " $results" + [pscustomobject]@{ + ComputerName = $computer + DataCollectorSet = $setname + DataCollector = $collectorname + Name = $counterName + Status = "Removed" + } + } + catch { + Stop-Function -Message "Failure importing $Countername to $computer." -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaPfDataCollectorSet.ps1 b/functions/Remove-DbaPfDataCollectorSet.ps1 new file mode 100644 index 000000000000..894ea22ac481 --- /dev/null +++ b/functions/Remove-DbaPfDataCollectorSet.ps1 @@ -0,0 +1,135 @@ +function Remove-DbaPfDataCollectorSet { + <# + .SYNOPSIS + Removes a Performance Monitor Data Collector Set + + .DESCRIPTION + Removes a Performance Monitor Data Collector Set. When removing data collector sets from the local instance, Run As Admin is required. + + .PARAMETER ComputerName + The target computer. Defaults to localhost. + + .PARAMETER Credential + Allows you to login to $ComputerName using alternative credentials. To use: + + $cred = Get-Credential, then pass $cred object to the -Credential parameter. + + .PARAMETER CollectorSet + The name of the Collector Set to remove. + + .PARAMETER InputObject + Accepts the object output by Get-DbaPfDataCollectorSet via the pipeline. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: PerfMon + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaPfDataCollectorSet + + .EXAMPLE + Remove-DbaPfDataCollectorSet + + Prompts for confirmation then removes all ready Collectors on localhost. + + .EXAMPLE + Remove-DbaPfDataCollectorSet -ComputerName sql2017 -Confirm:$false + + Attempts to remove all ready Collectors on localhost and does not prompt to confirm. + + .EXAMPLE + Remove-DbaPfDataCollectorSet -ComputerName sql2017, sql2016 -Credential (Get-Credential) -CollectorSet 'System Correlation' + + Prompts for confirmation then removes the 'System Correlation' Collector on sql2017 and sql2016 using alternative credentials. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Remove-DbaPfDataCollectorSet + + Removes the 'System Correlation' Collector. + + .EXAMPLE + Get-DbaPfDataCollectorSet -CollectorSet 'System Correlation' | Stop-DbaPfDataCollectorSet | Remove-DbaPfDataCollectorSet + + Stops and removes the 'System Correlation' Collector. + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param ( + [DbaInstance[]]$ComputerName=$env:COMPUTERNAME, + [PSCredential]$Credential, + [Alias("DataCollectorSet")] + [string[]]$CollectorSet, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + begin { + $setscript = { + $setname = $args + $collectorset = New-Object -ComObject Pla.DataCollectorSet + $collectorset.Query($setname, $null) + if ($collectorset.name -eq $setname) { + $null = $collectorset.Delete() + } + else { + Write-Warning "Data Collector Set $setname does not exist on $env:COMPUTERNAME." + } + } + } + process { + if (-not $InputObject -or ($InputObject -and (Test-Bound -ParameterName ComputerName))) { + foreach ($computer in $ComputerName) { + $InputObject += Get-DbaPfDataCollectorSet -ComputerName $computer -Credential $Credential -CollectorSet $CollectorSet + } + } + + if ($InputObject) { + if (-not $InputObject.DataCollectorSetObject) { + Stop-Function -Message "InputObject is not of the right type. Please use Get-DbaPfDataCollectorSet." + return + } + } + + # Check to see if its running first + foreach ($set in $InputObject) { + $setname = $set.Name + $computer = $set.ComputerName + $status = $set.State + + $null = Test-ElevationRequirement -ComputerName $computer -Continue + + Write-Message -Level Verbose -Message "$setname on $ComputerName is $status." + + if ($status -eq "Running") { + Stop-Function -Message "$setname on $computer is running. Use Stop-DbaPfDataCollectorSet to stop first." -Continue + } + + if ($Pscmdlet.ShouldProcess("$computer", "Removing collector set $setname")) { + Write-Message -Level Verbose -Message "Connecting to $computer using Invoke-Command." + try { + Invoke-Command2 -ComputerName $computer -Credential $Credential -ScriptBlock $setscript -ArgumentList $setname -ErrorAction Stop + [pscustomobject]@{ + ComputerName = $computer + Name = $setname + Status = "Removed" + } + } + catch { + Stop-Function -Message "Failure Removing $setname on $computer." -ErrorRecord $_ -Target $computer -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaSpn.ps1 b/functions/Remove-DbaSpn.ps1 index 2dc52f9325ca..1f307e42c9cc 100644 --- a/functions/Remove-DbaSpn.ps1 +++ b/functions/Remove-DbaSpn.ps1 @@ -1,11 +1,11 @@ -Function Remove-DbaSpn -{ -<# +#ValidationTags#FlowControl,Pipeline# +function Remove-DbaSpn { + <# .SYNOPSIS -Sets an SPN for a given service account in active directory, and also enables delegation to the same SPN +Removes an SPN for a given service account in active directory and also removes delegation to the same SPN, if found .DESCRIPTION -This function will connect to Active Directory and search for an account. If the account is found, it will attempt to remove the specified SPN. Once the SPN is removed, the function will also set delegation to that service. +This function will connect to Active Directory and search for an account. If the account is found, it will attempt to remove the specified SPN. Once the SPN is removed, the function will also remove delegation to that service. In order to run this function, the credential you provide must have write access to Active Directory. @@ -20,21 +20,24 @@ The account you want the SPN remove from .PARAMETER Credential The credential you want to use to connect to Active Directory to make the changes +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + .PARAMETER Confirm Turns confirmations before changes on or off - + .PARAMETER WhatIf -Shows what would happen if the command was executed - +Shows what would happen if the command was executed + .NOTES Tags: SPN Author: Drew Furgiuele (@pittfurg), http://www.port1433.com dbatools PowerShell module (https://dbatools.io) Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Remove-DbaSpn @@ -42,7 +45,12 @@ https://dbatools.io/Remove-DbaSpn .EXAMPLE Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -Connects to Active Directory and removes a provided SPN to the given account. +Connects to Active Directory and removes a provided SPN from the given account (and also the relative delegation) + +.EXAMPLE +Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -EnableException + +Connects to Active Directory and removes a provided SPN from the given account, suppressing all error messages and throw exceptions that can be caught instead .EXAMPLE Remove-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -Credential (Get-Credential) @@ -51,125 +59,128 @@ Connects to Active Directory and removes a provided SPN to the given account. Us .EXAMPLE Test-DbaSpn -ComputerName sql2005 | Where { $_.isSet -eq $true } | Remove-DbaSpn -WhatIf - -Connects to Active Directory and removes a provided SPN to the given account. Uses alternative account to connect to AD. + +Shows what would happen trying to remove all set SPNs for sql2005 and the relative delegations + +.EXAMPLE +Test-DbaSpn -ComputerName sql2005 | Where { $_.isSet -eq $true } | Remove-DbaSpn + +Removes all set SPNs for sql2005 and the relative delegations #> - [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] - param ( - [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] - [Alias("RequiredSPN")] - [string]$SPN, - [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] - [Alias("InstanceServiceAccount", "AccountName")] - [string]$ServiceAccount, - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] - [pscredential]$Credential - ) - - process - { - $OGServiceAccount = $ServiceAccount - if ($serviceaccount -like "*\*") - { - Write-Debug "Account provided in in domain\user format, stripping out domain info..." - $serviceaccount = ($serviceaccount.split("\"))[1] - } - if ($serviceaccount -like "*@*") - { - Write-Debug "Account provided in in user@domain format, stripping out domain info..." - $serviceaccount = ($serviceaccount.split("@"))[0] - } - - $root = ([ADSI]"LDAP://RootDSE").defaultNamingContext - $adsearch = New-Object System.DirectoryServices.DirectorySearcher - - if ($Credential) - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root), $($Credential.UserName), $($Credential.GetNetworkCredential().password) - } - else - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root) - } - - $adsearch.SearchRoot = $domain - $adsearch.Filter = $("(&(samAccountName={0}))" -f $serviceaccount) - Write-Verbose "Looking for account $serviceAccount..." - $result = $adsearch.FindOne() - - #did we find the server account? - - if ($result -eq $null) - { - Write-Warning "The account specified for the SPN ($serviceAccount) does not exist on the domain" - continue - } - else - { - # Cool! Remove an SPN - - $adentry = $result.GetDirectoryEntry() - $delegate = $true - - if ($PSCmdlet.ShouldProcess("$spn", "Removing SPN for service account")) - { - try - { - $null = $adentry.Properties['serviceprincipalname'].Remove($spn) - Write-Verbose "Remove SPN $spn for samaccount $serviceaccount" - $adentry.CommitChanges() - $set = $false - $status = "Successfully removed SPN" - $delegate = $true - } - catch - { - Write-Warning "Could not remove SPN. Error returned was: $_" - $set = $true - $status = "Failed to remove SPN" - $delegate = $false - } - - [pscustomobject]@{ - Name = $spn - ServiceAccount = $OGServiceAccount - Property = "servicePrincipalName" - IsSet = $set - Notes = $status - } - } - - if (!$delegate) { continue } - - # Don't forget delegation! - $adentry = $result.GetDirectoryEntry() - if ($PSCmdlet.ShouldProcess("$spn", "Removing delegation for service account for SPN")) - { - try - { - $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Remove($spn) - Write-Verbose "Removed kerberos delegation for $spn for samaccount $serviceaccount" - $adentry.CommitChanges() - $set = $false - $status = "Successfully removed delegation" - } - catch - { - Write-Warning "Could not remove delegation. Error returned was: $_" - $set = $true - $status = "Failed to remove delegation" - } - - [pscustomobject]@{ - Name = $spn - ServiceAccount = $OGServiceAccount - Property = "msDS-AllowedToDelegateTo" - IsSet = $set - Notes = $status - } - } - } - } -} + [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("RequiredSPN")] + [string]$SPN, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("InstanceServiceAccount", "AccountName")] + [string]$ServiceAccount, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] + [PSCredential]$Credential, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + Write-Message -Message "Looking for account $ServiceAccount..." -Level Verbose + $searchfor = 'User' + if ($ServiceAccount.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $ServiceAccount -Type $searchfor -Credential $Credential -EnableException + } + catch { + Stop-Function -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($ServiceAccount). $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + if ($Result.Count -gt 0) { + try { + $adentry = $Result.GetUnderlyingObject() + } + catch { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has been found, but you don't have enough permission to inspect its properties $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + } + else { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has not been found" -EnableException $EnableException -Target $ServiceAccount + } + + # Cool! Remove an SPN + $delegate = $true + $spnadobject = $adentry.Properties['servicePrincipalName'] + + if ($spnadobject -notcontains $spn) { + Write-Message -Level Warning -Message "SPN $SPN not found" + $status = "SPN not found" + $set = $false + } + + if ($PSCmdlet.ShouldProcess("$spn", "Removing SPN for service account")) { + try { + if ($spnadobject -contains $spn) { + $null = $spnadobject.Remove($spn) + $adentry.CommitChanges() + Write-Message -Message "Remove SPN $spn for $serviceaccount" -Level Verbose + $set = $false + $status = "Successfully removed SPN" + } + } + catch { + Write-Message -Message "Could not remove SPN. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccountWrite + $set = $true + $status = "Failed to remove SPN" + $delegate = $false + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "servicePrincipalName" + IsSet = $set + Notes = $status + } + } + # if we removed the SPN, we should clean up also the delegation + if ($PSCmdlet.ShouldProcess("$spn", "Removing delegation for service account for SPN")) { + # if we didn't remove the SPN we shouldn't do anything + if ($delegate) { + # even if we removed the SPN, delegation could have been not set at all. We should not raise an error + if ($adentry.Properties['msDS-AllowedToDelegateTo'] -notcontains $spn) { + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $false + Notes = "Delegation not found" + } + } + else { + # we indeed need the cleanup + try { + $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Remove($spn) + $adentry.CommitChanges() + Write-Message -Message "Removed kerberos delegation $spn for $ServiceAccount" -Level Verbose + $set = $false + $status = "Successfully removed delegation" + } + catch { + Write-Message -Message "Could not remove delegation. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $true + $status = "Failed to remove delegation" + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $set + Notes = $status + } + } + } + + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaTrace.ps1 b/functions/Remove-DbaTrace.ps1 new file mode 100644 index 000000000000..812f23aeb3bd --- /dev/null +++ b/functions/Remove-DbaTrace.ps1 @@ -0,0 +1,99 @@ +function Remove-DbaTrace { + <# + .SYNOPSIS + Stops and closes the specified trace and deletes its definition from the server. + + .DESCRIPTION + Stops and closes the specified trace and deletes its definition from the server. + + .PARAMETER SqlInstance + The target SQL Server instance + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Id + A list of trace ids + + .PARAMETER InputObject + Internal parameter for piping + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Security, Trace + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Remove-DbaTrace -SqlInstance sql2008 + + Stops and removes all traces on sql2008 + + .EXAMPLE + Remove-DbaTrace -SqlInstance sql2008 -Id 1 + + Stops and removes all trace with ID 1 on sql2008 + + .EXAMPLE + Get-DbaTrace -SqlInstance sql2008 | Out-GridView -PassThru | Remove-DbaTrace + + Stops and removes selected traces on sql2008 + +#> + [CmdletBinding()] + Param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [int[]]$Id, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + if (-not $InputObject -and $SqlInstance) { + $InputObject = Get-DbaTrace -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Id $Id + } + + foreach ($trace in $InputObject) { + if (-not $trace.id -and -not $trace.Parent) { + Stop-Function -Message "Input is of the wrong type. Use Get-DbaTrace." -Continue + return + } + + $server = $trace.Parent + $traceid = $trace.id + $default = Get-DbaTrace -SqlInstance $server -Default + + if ($default.id -eq $traceid) { + Stop-Function -Message "The default trace on $server cannot be stopped. Use Set-DbaSpConfigure to turn it off." -Continue + } + + $stopsql = "sp_trace_setstatus $traceid, 0" + $removesql = "sp_trace_setstatus $traceid, 2" + + try { + $server.Query($stopsql) + if (Get-DbaTrace -SqlInstance $server -Id $traceid) { + $server.Query($removesql) + } + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Id = $traceid + Status = "Stopped, closed and deleted" + } + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $server -Continue + return + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaXESession.ps1 b/functions/Remove-DbaXESession.ps1 new file mode 100644 index 000000000000..501b39945f9b --- /dev/null +++ b/functions/Remove-DbaXESession.ps1 @@ -0,0 +1,137 @@ +function Remove-DbaXESession { + <# + .SYNOPSIS + Removes Extended Events sessions. + + .DESCRIPTION + This script removes Extended Events sessions on a SQL Server instance. + + .PARAMETER SqlInstance + Target SQL Server. You must have sysadmin access and server version must be SQL Server version 2008 or higher. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Session + Specifies a list of Extended Events sessions to remove. + + .PARAMETER AllSessions + If this switch is enabled, all Extended Events sessions will be removed except the packaged sessions AlwaysOn_health, system_health, telemetry_xevents. + + .PARAMETER InputObject + Accepts a collection of XEsession objects as output by Get-DbaXESession. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Remove-DbaXESession + + .EXAMPLE + Remove-DbaXESession -SqlInstance sql2012 -AllSessions + + Removes all Extended Event Session on the sqlserver2014 instance. + + .EXAMPLE + Remove-DbaXESession -SqlInstance sql2012 -Session xesession1,xesession2 + + Removes the xesession1 and xesession2 Extended Event sessions. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2017 | Remove-DbaXESession -Confirm:$false + + Removes all sessions from sql2017, bypassing prompts. + + .EXAMPLE + Get-DbaXESession -SqlInstance sql2012 -Session xesession1 | Remove-DbaXESession + + Removes the sessions returned from the Get-DbaXESession function. + #> + [CmdletBinding(DefaultParameterSetName = 'Session', SupportsShouldProcess, ConfirmImpact = 'High')] + param ( + [parameter(Position = 1, Mandatory, ParameterSetName = 'Session')] + [parameter(Position = 1, Mandatory, ParameterSetName = 'All')] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [parameter(ParameterSetName = 'Session')] + [parameter(ParameterSetName = 'All')] + [PSCredential]$SqlCredential, + [parameter(Mandatory, ParameterSetName = 'Session')] + [Alias("Sessions")] + [object[]]$Session, + [parameter(Mandatory, ParameterSetName = 'All')] + [switch]$AllSessions, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')] + [Microsoft.SqlServer.Management.XEvent.Session[]]$InputObject, + [switch]$EnableException + ) + + begin { + # Remove each XESession + function Remove-XESessions { + [CmdletBinding()] + param ([Microsoft.SqlServer.Management.XEvent.Session[]]$xeSessions) + + foreach ($xe in $xeSessions) { + $instance = $xe.Parent.Name + $session = $xe.Name + + if ($Pscmdlet.ShouldProcess("$instance", "Removing XEvent Session $session")) { + try { + $xe.Drop() + [pscustomobject]@{ + ComputerName = $xe.Parent.NetName + InstanceName = $xe.Parent.ServiceName + SqlInstance = $xe.Parent.DomainInstanceName + Session = $session + Status = "Removed" + } + } + catch { + Stop-Function -Message "Could not remove XEvent Session on $instance" -Target $session -ErrorRecord $_ -Continue + } + } + } + } + } + + process { + if ($InputObject) { + # avoid the collection issue + $sessions = Get-DbaXESession -SqlInstance $InputObject.Parent -Session $InputObject.Name + foreach ($item in $sessions) { + Remove-XESessions $item + } + } + else { + foreach ($instance in $SqlInstance) { + $xeSessions = Get-DbaXESession -SqlInstance $instance -SqlCredential $SqlCredential + + # Filter xeSessions based on parameters + if ($Session) { + $xeSessions = $xeSessions | Where-Object { $_.Name -in $Session } + } + elseif ($AllSessions) { + $systemSessions = @('AlwaysOn_health', 'system_health', 'telemetry_xevents') + $xeSessions = $xeSessions | Where-Object { $_.Name -notin $systemSessions } + } + + Remove-XESessions $xeSessions + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-DbaXESmartTarget.ps1 b/functions/Remove-DbaXESmartTarget.ps1 new file mode 100644 index 000000000000..f3e2ec27ab5c --- /dev/null +++ b/functions/Remove-DbaXESmartTarget.ps1 @@ -0,0 +1,63 @@ +function Remove-DbaXESmartTarget { + <# + .SYNOPSIS + Removes an XESmartTarget PowerShell Job. + + .DESCRIPTION + Removes an XESmartTarget PowerShell Job. + + .PARAMETER InputObject + Specifies one or more XESmartTarget job objects as output by Get-DbaXESmartTarget. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: ExtendedEvent, XE, XEvent + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + SmartTarget: by Gianluca Sartori (@spaghettidba) + + .LINK + https://dbatools.io/Remove-DbaXESmartTarget + https://github.com/spaghettidba/XESmartTarget/wiki + + .EXAMPLE + Get-DbaXESmartTarget | Remove-DbaXESmartTarget + + Removes all XESmartTarget jobs. + + .EXAMPLE + Get-DbaXESmartTarget | Where-Object Id -eq 2 | Remove-DbaXESmartTarget + + Removes a specific XESmartTarget job. + #> + [CmdletBinding(SupportsShouldProcess)] + param ( + [parameter(Mandatory, ValueFromPipeline)] + [object[]]$InputObject, + [switch]$EnableException + ) + process { + if ($Pscmdlet.ShouldProcess("localhost", "Removing job $id")) { + try { + $id = $InputObject.Id + Write-Message -Level Output -Message "Removing job $id, this may take a couple minutes." + Get-Job -ID $InputObject.Id | Remove-Job -Force + Write-Message -Level Output -Message "Successfully removed $id." + } + catch { + Stop-Function -Message "Failure" -ErrorRecord $_ + } + } + } +} \ No newline at end of file diff --git a/functions/Remove-SqlDatabaseSafely.ps1 b/functions/Remove-SqlDatabaseSafely.ps1 deleted file mode 100644 index e00957e44be6..000000000000 --- a/functions/Remove-SqlDatabaseSafely.ps1 +++ /dev/null @@ -1,768 +0,0 @@ -Function Remove-SqlDatabaseSafely -{ -<# -.SYNOPSIS -Safely removes a SQL Database and creates an Agent Job to restore it - -.DESCRIPTION -Performs a DBCC CHECKDB on the database, backs up the database with Checksum and verify only to a Final Backup location, creates an Agent Job to restore from that backup, Drops the database, runs the agent job to restore the database, -performs a DBCC CHECKDB and drops the database - -By default the initial DBCC CHECKDB is performed -By default the jobs and databases are created on the same server. Use -Destination to use a seperate server - -It will start the SQL Agent Service on the Destination Server if it is not running - -.PARAMETER SqlServer -The SQL Server instance holding the databases to be removed.You must have sysadmin access and server version must be SQL Server version 2000 or higher. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. - -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. - -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER Destination -If specified this is the server that the Agent Jobs will be created on. By default this is the same server as the SQLServer.You must have sysadmin access and server version must be SQL Server version 2000 or higher. - -.PARAMETER Databases -The database name to remove or an array of database names eg $Databases = 'DB1','DB2','DB3' - -.PARAMETER NoCheck -If this switch is used the initial DBCC CHECK DB will be skipped. This will make the process quicker but will also create an agent job to restore a database backup containing a corrupt database. -A second DBCC CHECKDB is performed on the restored database so you will still be notified BUT USE THIS WITH CARE - -.PARAMETER BackupFolder -Final (Golden) Backup Folder for storing the backups. Be Careful if you are using a source and destination server that you use the full UNC path eg \\SERVER1\BACKUPSHARE\ - -.PARAMETER JobOwner -The account that will own the Agent Jobs - Defaults to sa - -.PARAMETER UseDefaultFilePaths -Use the instance default file paths for the mdf and ldf files to restore the database if not set will use the original file paths - -.PARAMETER CategoryName -The Category Name for the Agent Job that gets created defaults to Rationalisation - -.PARAMETER DBCCErrorFolder -FolderPath for DBCC Error Output - defaults to C:\temp - -.PARAMETER BackupCompression -The setting for the backup compression for the backup defaults to the default setting for the server but accepts On or Off to override that setting - -.PARAMETER AllDatabases -Runs the script for every user databases on a server - Useful when decomissioning a server - That would need a DestinationServer set - -.PARAMETER Force -This switch will continue to perform rest of the actions and will create an Agent Job with DBCCERROR in the name and a Backup file with DBCC in the name - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.NOTES -Tags: DisasterRecovery, Backup, Restore -Original Author: Rob Sewell @SQLDBAWithBeard, sqldbawithabeard.com - With huge thanks to Grant Fritchey and his verify your backups video - Take a look its only 3 minutes long - http://sqlps.io/backuprant - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Remove-SqlDatabaseSafely - -.EXAMPLE -Remove-SqlDatabaseSafely -SqlServer 'Fade2Black' -Databases RideTheLightning -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' - -For the database RideTheLightning on the server Fade2Black Will perform a DBCC CHECKDB and if there are no errors -backup the database to the folder C:\MSSQL\Backup\Rationalised - DO NOT DELETE. It will then create an Agent Job to restore the database -from that backup. It will drop the database, run the agent job to restore it, perform a DBCC ChECK DB and then drop the database. - -Any DBCC errors will be written to your documents folder - -.EXAMPLE -$Databases = 'DemoNCIndex','RemoveTestDatabase' -Remove-SqlDatabaseSafely -SqlServer 'Fade2Black' -Databases $Databases -BackupFolder 'C:\MSSQL\Backup\Rationalised - DO NOT DELETE' - -For the databases 'DemoNCIndex','RemoveTestDatabase' on the server Fade2Black Will perform a DBCC CHECKDB and if there are no errors -backup the database to the folder C:\MSSQL\Backup\Rationalised - DO NOT DELETE. It will then create an Agent Job for each database -to restore the database from that backup. It will drop the database, run the agent job, perform a DBCC ChECK DB and then drop the database - -Any DBCC errors will be written to your documents folder - -.EXAMPLE -Remove-SqlDatabaseSafely -SqlServer 'Fade2Black' -DestinationServer JusticeForAll -Databases RideTheLightning -BackupFolder '\\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE' - -For the database RideTheLightning on the server Fade2Black Will perform a DBCC CHECKDB and if there are no errors -backup the database to the folder \\BACKUPSERVER\BACKUPSHARE\MSSQL\Rationalised - DO NOT DELETE It will then create an Agent Job on the server -JusticeForAll to restore the database from that backup. It will drop the database on Fade2Black, run the agent job to restore it on JusticeForAll, -perform a DBCC ChECK DB and then drop the database - -Any DBCC errors will be written to your documents folder -.EXAMPLE -Remove-SqlDatabaseSafely -SqlServer IronMaiden -Databases $Databases -DestinationServer TheWildHearts -DBCCErrorFolder C:\DBCCErrors -BackupFolder z:\Backups -NoDBCCCheck -UseDefaultFilePaths -JobOwner 'THEBEARD\Rob' - -For the databases $Databases on the server IronMaiden Will NOT perform a DBCC CHECKDB -It will backup the databases to the folder Z:\Backups It will then create an Agent Job on the server with a Job Owner of THEBEARD\Rob -TheWildHearts to restore the database from that backup using the instance default filepaths. -It will drop the database on IronMaiden, run the agent job to restore it on TheWildHearts using the default file paths for the instance, perform -a DBCC ChECK DB and then drop the database - -Any DBCC errors will be written to your documents folder - -.EXAMPLE -Remove-SqlDatabaseSafely -SqlServer IronMaiden -Databases $Databases -DestinationServer TheWildHearts -DBCCErrorFolder C:\DBCCErrors -BackupFolder z:\Backups -UseDefaultFilePaths -ContinueAfterDbccError - -For the databases $Databases on the server IronMaiden will backup the databases to the folder Z:\Backups It will then create an Agent Job -TheWildHearts to restore the database from that backup using the instance default filepaths. -It will drop the database on IronMaiden, run the agent job to restore it on TheWildHearts using the default file paths for the instance, perform -a DBCC ChECK DB and then drop the database - -If there is a DBCC Error it will continue to perform rest of the actions and will create an Agent Job with DBCCERROR in the name and a Backup file with DBCCError in the name - - -#> - [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [parameter(Mandatory = $false)] - [object]$SqlCredential, - [parameter(Mandatory = $false)] - [object]$Destination = $SqlServer, - [parameter(Mandatory = $false)] - [switch]$NoCheck, - [parameter(Mandatory = $true)] - [string]$BackupFolder, - [parameter(Mandatory = $false)] - [string]$CategoryName = 'Rationalisation', - [parameter(Mandatory = $false)] - [string]$JobOwner, - [parameter(Mandatory = $false)] - [string]$DbccErrorFolder = [Environment]::GetFolderPath("mydocuments"), - [parameter(Mandatory = $false)] - [switch]$AllDatabases, - [ValidateSet("Default", "On", "Of")] - [string]$BackupCompression = 'Default', - #[Alias("UseDefaultFilePaths")] - [switch]$ReuseSourceFolderStructure, - [switch]$Force - - ) - - DynamicParam { if ($sqlserver) { return Get-ParamSqlDatabases -SqlServer $sqlserver -SqlCredential $SqlCredential } } - - BEGIN - { - $databases = $psboundparameters.Databases - - if ($AllDatabases -eq $false -and $databases.length -eq 0) - { - throw "You must specify at least one database. Use -Databases or -AllDatabases." - } - - $sourceserver = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $sqlCredential -ParameterConnection - - - if ($SqlServer -ne $destination) - { - $destserver = Connect-SqlServer -SqlServer $destination -SqlCredential $sqlCredential - - $sourcenb = $sourceserver.ComputerNamePhysicalNetBIOS - $destnb = $sourceserver.ComputerNamePhysicalNetBIOS - - if ($BackupFolder.StartsWith("\\") -eq $false -and $sourcenb -ne $destnb) - { - throw "Backup folder must be a network share if the source and destination servers are not the same." - } - } - else - { - $destserver = $sourceserver - } - - $source = $sourceserver.DomainInstanceName - $destination = $destserver.DomainInstanceName - - if ($jobowner.Length -eq 0) - { - $jobowner = Get-SqlSaLogin $destserver - } - - if ($alldatabases -or $databases.count -eq 0) - { - $databases = ($sourceserver.databases | Where-Object{ $_.IsSystemObject -eq $false -and ($_.Status -match 'Offline') -eq $false}).Name - } - - if (!(Test-SqlPath -SqlServer $destserver -Path $backupFolder)) - { - $serviceaccount = $destserver.ServiceAccount - throw "Can't access $backupFolder Please check if $serviceaccount has permissions" - } - - $jobname = "Rationalised Final Database Restore for $dbname" - $jobStepName = "Restore the $dbname database from Final Backup" - - if (!($destserver.Logins | Where-Object{ $_.Name -eq $jobowner })) - { - throw "$destination does not contain the login $jobowner - Please fix and try again - Aborting" - } - - function Start-SqlAgent - { - [CmdletBinding(SupportsShouldProcess = $true)] - param() - if ($destserver.VersionMajor -eq 8) - { - $serviceName = 'MSSQLSERVER' - } - else - { - $instance = $destserver.InstanceName - if ($instance.length -eq 0) { $instance = "MSSQLSERVER" } - $serviceName = "SQL Server Agent ($instance)" - } - - if ($Pscmdlet.ShouldProcess($destination, "Starting Sql Agent")) - { - try - { - $ipaddr = Resolve-SqlIpAddress $destserver - $agentservice = Get-Service -ComputerName $ipaddr -DisplayName $serviceName - - if ($agentservice.Status -ne 'Running') - { - $agentservice.Start() - $timeout = New-Timespan -seconds 60 - $sw = [diagnostics.stopwatch]::StartNew() - $agentstatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status - while ($AgentStatus -ne 'Running' -and $sw.elapsed -lt $timeout) - { - $agentStatus = (Get-Service -ComputerName $ipaddr -DisplayName $serviceName).Status - } - } - } - - catch - { - Write-Exception $_ - } - - if ($agentservice.Status -ne 'Running') - { - throw "Cannot start Agent Service on $destination - Aborting" - } - } - } - - Function Start-DbccCheck - { - - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [object]$server, - [string]$dbname - ) - - $servername = $server.name - $db = $server.databases[$dbname] - - if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $servername")) - { - try - { - $null = $db.CheckTables('None') - Write-Output "Dbcc CHECKDB finished successfully for $dbname on $servername" - } - - catch - { - Write-Warning "DBCC CHECKDB failed" - Write-Exception $_ - - if ($force) - { - return $true - } - else - { - return $false - } - } - } - } - - Function New-SqlAgentJobCategory - { - - [CmdletBinding(SupportsShouldProcess = $true)] - param ([string]$categoryname, - [object]$jobServer) - - if (!$jobServer.JobCategories[$categoryname]) - { - if ($Pscmdlet.ShouldProcess($sourceserver, "Running dbcc check on $dbname on $sourceserver")) - { - try - { - Write-Output "Creating Agent Job Category $categoryname" - $category = New-Object Microsoft.SqlServer.Management.Smo.Agent.JobCategory - $category.Parent = $jobServer - $category.Name = $categoryname - $category.Create() - Write-Output "Created Agent Job Category $categoryname" - } - catch - { - Write-Exception $_ - throw "FAILED : To Create Agent Job Category $categoryname - Aborting" - } - } - } - } - - Function Restore-Database - { - <# - .SYNOPSIS - Internal function. Restores .bak file to Sql database. Creates db if it doesn't exist. $filestructure is - a custom object that contains logical and physical file locations. - - ALTERED To Add TSql switch and remove norecovery switch default - #> - - param ( - [Parameter(Mandatory = $true)] - [Alias('ServerInstance', 'SqlInstance')] - [object]$server, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$dbname, - [Parameter(Mandatory = $true)] - [string]$backupfile, - [string]$filetype = 'Database', - [Parameter(Mandatory = $true)] - [object]$filestructure, - [switch]$norecovery, - [System.Management.Automation.PSCredential]$sqlCredential, - [switch]$TSql = $false - ) - - $server = Connect-SqlServer -SqlServer $server -SqlCredential $sqlCredential - $servername = $server.name - $server.ConnectionContext.StatementTimeout = 0 - $restore = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' - $restore.ReplaceDatabase = $true - - foreach ($file in $filestructure.values) - { - $movefile = New-Object 'Microsoft.SqlServer.Management.Smo.RelocateFile' - $movefile.LogicalFileName = $file.logical - $movefile.PhysicalFileName = $file.physical - $null = $restore.RelocateFiles.Add($movefile) - } - - try - { - if ($TSql) - { - $restore.PercentCompleteNotification = 1 - $restore.add_Complete($complete) - $restore.ReplaceDatabase = $true - $restore.Database = $dbname - $restore.Action = $filetype - $restore.NoRecovery = $norecovery - $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem - $device.name = $backupfile - $device.devicetype = 'File' - $restore.Devices.Add($device) - $restorescript = $restore.script($server) - return $restorescript - } - else - { - $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) - } - $restore.add_PercentComplete($percent) - $restore.PercentCompleteNotification = 1 - $restore.add_Complete($complete) - $restore.ReplaceDatabase = $true - $restore.Database = $dbname - $restore.Action = $filetype - $restore.NoRecovery = $norecovery - $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem - $device.name = $backupfile - $device.devicetype = 'File' - $restore.Devices.Add($device) - - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) - $restore.sqlrestore($server) - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -status 'Complete' -Completed - - return $true - } - } - catch - { - Write-Error "Restore failed: $($_.Exception)" - return $false - } - } - - } - PROCESS - { - Start-SqlAgent - - $start = Get-Date - Write-Output "Starting Rationalisation Script at $start" - - foreach ($dbname in $databases) - { - - $db = $sourceserver.databases[$dbname] - - # The db check is needed when the number of databases exceeds 255, then it's no longer autopopulated - if (!$db) - { - Write-Warning "$dbname does not exist on $source. Aborting routine for this database" - continue - } - - $jobname = "Rationalised Database Restore Script for $dbname" - $jobStepName = "Restore the $dbname database from Final Backup" - $jobServer = $destserver.JobServer - - if ($jobServer.Jobs[$jobname].count -gt 0) - { - if ($force -eq $false) - { - Write-Warning "FAILED: The Job $jobname already exists. Have you done this before? Rename the existing job and try again or use -Force to drop and recreate." - continue - } - else - { - if ($Pscmdlet.ShouldProcess($dbname, "Dropping $jobname on $source")) - { - Write-Output "Dropping $jobname on $source" - $jobServer.Jobs[$jobname].Drop() - $jobServer.Jobs.Refresh() - } - } - } - - - Write-Output "Starting Rationalisation of $dbname" - ## if we want to Dbcc before to abort if we have a corrupt database to start with - if ($NoCheck -eq $false) - { - if ($Pscmdlet.ShouldProcess($dbname, "Running dbcc check on $dbname on $source")) - { - Write-Output "Starting Dbcc CHECKDB for $dbname on $source" - $dbccgood = Start-DbccCheck -Server $sourceserver -DBName $dbname - - if ($dbccgood -eq $false) - { - if ($force -eq $false) - { - Write-Output "DBCC failed for $dbname (you should check that). Aborting routine for this database" - continue - } - else - { - Write-Output "DBCC failed, but Force specified. Continuing." - } - } - } - } - - if ($Pscmdlet.ShouldProcess($source, "Backing up $dbname")) - { - Write-Output "Starting Backup for $dbname on $source" - ## Take a Backup - try - { - $timenow = [DateTime]::Now.ToString('yyyyMMdd_HHmmss') - $backup = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Backup - $backup.Action = [Microsoft.SqlServer.Management.SMO.BackupActionType]::Database - $backup.BackupSetDescription = "Final Full Backup of $dbname Prior to Dropping" - $backup.Database = $dbname - $backup.Checksum = $True - if ($sourceserver.versionMajor -gt 9) - { - $backup.CompressionOption = $BackupCompression - } - if ($force -and $dbccgood -eq $false) - { - - $filename = "$backupFolder\$($dbname)_DBCCERROR_$timenow.bak" - } - else - { - $filename = "$backupFolder\$($dbname)_Final_Before_Drop_$timenow.bak" - } - - $devicetype = [Microsoft.SqlServer.Management.Smo.DeviceType]::File - $backupDevice = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem($filename, $devicetype) - - $backup.Devices.Add($backupDevice) - #Progress - $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { - Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) - } - $backup.add_PercentComplete($percent) - $backup.add_Complete($complete) - Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) - $backup.SqlBackup($sourceserver) - $null = $backup.Devices.Remove($backupDevice) - Write-Progress -id 1 -activity "Backing up database $dbname on $source to $filename" -status "Complete" -Completed - Write-Output "Backup Completed for $dbname on $source " - - Write-Output "Running Restore Verify only on Backup of $dbname on $source" - try - { - $restoreverify = New-Object 'Microsoft.SqlServer.Management.Smo.Restore' - $restoreverify.Database = $dbname - $restoreverify.Devices.AddDevice($filename, $devicetype) - $result = $restoreverify.SqlVerify($sourceserver) - - if ($result -eq $false) - { - Write-Warning "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database" - continue - } - - Write-Output "Restore Verify Only for $filename Succeeded " - } - catch - { - Write-Warning "FAILED : Restore Verify Only failed for $filename on $server - aborting routine for this database" - Write-Exception $_ - continue - } - } - catch - { - Write-Exception $_ - Write-Warning "FAILED : To backup database $dbname on $server - aborting routine for this database" - continue - } - } - - if ($Pscmdlet.ShouldProcess($destination, "Creating Automated Restore Job from Golden Backup for $dbname on $destination")) - { - Write-Output "Creating Automated Restore Job from Golden Backup for $dbname on $destination " - try - { - if ($force -eq $true -and $dbccgood -eq $false) - { - $jobName = $jobname -replace "Rationalised", "DBCC ERROR" - } - - ## Create an agent job to restore the database - $job = New-Object Microsoft.SqlServer.Management.Smo.Agent.Job $jobServer, $jobname - $job.Name = $jobname - $job.OwnerLoginName = $jobowner - $job.Description = "This job will restore the $dbname database using the final backup located at $filename" - - ## Create a Job Category - if (!$jobServer.JobCategories[$categoryname]) - { - New-SqlAgentJobCategory -JobServer $jobServer -categoryname $categoryname - } - - $job.Category = $categoryname - try - { - if ($Pscmdlet.ShouldProcess($destination, "Creating Agent Job on $destination")) - { - Write-Output "Created Agent Job $jobname on $destination " - $job.Create() - } - } - catch - { - Write-Warning "FAILED : To Create Agent Job $jobname on $destination - aborting routine for this database" - Write-Exception $_ - continue - } - - ## Create Job Step - ## Aarons Suggestion: In the restore script, add a comment block that tells the last known size of each file in the database. - ## Suggestion check for disk space before restore - ## Create Restore Script - try - { - $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore - $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem $filename, 'FILE' - $restore.Devices.Add($device) - try - { - $filelist = $restore.ReadFileList($destserver) - } - - catch - { - throw 'File list could not be determined. This is likely due to connectivity issues or tiemouts with the Sql Server, the database version is incorrect, or the Sql Server service account does not have access to the file share. Script terminating.' - } - - $filestructure = Get-OfflineSqlFileStructure $destserver $dbname $filelist $ReuseSourceFolderStructure - - #if ($filestructure -eq $false) - #{ - # Write-Warning "$dbname contains FILESTREAM and filestreams are not supported by destination server. Skipping." - # continue - #} - - $jobStepCommmand = Restore-Database $destserver $dbname $filename "Database" $filestructure -TSql -ErrorAction Stop - $jobStep = new-object Microsoft.SqlServer.Management.Smo.Agent.JobStep $job, $jobStepName - $jobStep.SubSystem = 'TransactSql' # 'PowerShell' - $jobStep.DatabaseName = 'master' - $jobStep.Command = $jobStepCommmand - $jobStep.OnSuccessAction = 'QuitWithSuccess' - $jobStep.OnFailAction = 'QuitWithFailure' - if ($Pscmdlet.ShouldProcess($destination, "Creating Agent JobStep on $destination")) - { - $null = $jobStep.Create() - } - $jobStartStepid = $jobStep.ID - Write-Output "Created Agent JobStep $jobStepName on $destination " - } - catch - { - Write-Warning "FAILED : To Create Agent JobStep $jobStepName on $destination - Aborting" - Write-Exception $_ - continue - } - if ($Pscmdlet.ShouldProcess($destination, "Applying Agent Job $jobname to $destination")) - { - $job.ApplyToTargetServer($destination) - $job.StartStepID = $jobStartStepid - $job.Alter() - } - } - catch - { - Write-Warning "FAILED : To Create Agent Job $jobname on $destination - aborting routine for $dbname" - Write-Exception $_ - continue - } - } - - if ($Pscmdlet.ShouldProcess($destination, "Dropping Database $dbname on $sourceserver")) - { - ## Drop the database - try - { - # Remove-SqlDatabase is a function in SharedFunctions.ps1 that tries 3 different ways to drop a database - Remove-SqlDatabase -SqlServer $sourceserver -DbName $dbname - Write-Output "Dropped $dbname Database on $source prior to running the Agent Job" - } - catch - { - Write-Warning "FAILED : To Drop database $dbname on $server - aborting routine for $dbname" - Write-Exception $_ - continue - } - } - - if ($Pscmdlet.ShouldProcess($destination, "Running Agent Job on $destination to restore $dbname")) - { - ## Run the restore job to restore it - Write-Output "Starting $jobname on $destination " - try - { - $job = $destserver.JobServer.Jobs[$jobname] - $job.Start() - $job.Refresh() - $status = $job.CurrentRunStatus - - while ($status -ne 'Idle') - { - Write-Output "Restore Job for $dbname on $destination is $status..." - Start-Sleep -Seconds 15 - $job.Refresh() - $status = $job.CurrentRunStatus - } - - Write-Output "Restore Job $jobname has completed on $destination " - Write-Output "Sleeping for a few seconds to ensure the next step (DBCC) succeeds" - Start-Sleep -Seconds 10 ## This is required to ensure the next DBCC Check succeeds - } - catch - { - Write-Warning "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname" - Write-Exception $_ - continue - } - - if ($job.LastRunOutcome -ne 'Succeeded') - { - # LOL, love the plug. - Write-Warning "FAILED : Restore Job $jobname failed on $destination - aborting routine for $dbname" - Write-Warning "Check the Agent Job History on $destination - if you have SSMS2016 July release or later" - Write-Warning "Get-SqlAgentJobHistory -JobName $jobname -ServerInstance $destination -OutcomesType Failed " - continue - } - } - - ## Run a Dbcc No choice here - if ($Pscmdlet.ShouldProcess($dbname, "Running Dbcc CHECKDB on $dbname on $destination")) - { - Write-Output "Starting Dbcc CHECKDB for $dbname on $destination" - $null = Start-DbccCheck -Server $destserver -DbName $dbname - } - - if ($Pscmdlet.ShouldProcess($dbname, "Dropping Database $dbname on $destination")) - { - ## Drop the database - try - { - $null = Remove-SqlDatabase -SqlServer $sourceserver -DbName $dbname - Write-Output "Dropped $dbname Database on $destination" - } - catch - { - Write-Warning "FAILED : To Drop database $dbname on $destination - Aborting" - Write-Exception $_ - continue - } - } - Write-Output "Rationalisation Finished for $dbname" - } - } - - END - { - $sourceserver.ConnectionContext.Disconnect() - $destserver.ConnectionContext.Disconnect() - - if ($Pscmdlet.ShouldProcess("console", "Showing final message")) - { - $End = Get-Date - Write-Output "Finished at $End" - $Duration = $End - $start - Write-Output "Script Duration: $Duration" - } - } -} diff --git a/functions/Remove-SqlOrphanUser.ps1 b/functions/Remove-SqlOrphanUser.ps1 deleted file mode 100644 index ffcc49bdace3..000000000000 --- a/functions/Remove-SqlOrphanUser.ps1 +++ /dev/null @@ -1,381 +0,0 @@ -Function Remove-SqlOrphanUser -{ -<# -.SYNOPSIS -Drop orphan users with no existing login to map - -.DESCRIPTION -An orphan user is defined by a user that does not have their matching login. (Login property = ""). -If user is the owner of the schema with the same name and if if the schema does not have any underlying objects the schema will be dropped. -If user owns more than one schema, the owner of the schemas that does not have the same name as the user, will be changed to 'dbo'. If schemas have underlying objects, you must specify the -Force parameter so the user can be dropped. -If exists a login to map the drop will not be performed unless you specify the -Force parameter (only when calling from Repair-SqlOrphanUser. - -.PARAMETER SqlServer -The SQL Server instance. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. - -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER Users -List of users to remove - -.PARAMETER Force -If exists any schema which owner is the User, this will force the change of the owner to 'dbo'. -If exists a login to map the drop will not be performed unless you specify this parameter. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.NOTES -Tags: Orphan -Original Author: Claudio Silva (@ClaudioESSilva) -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Remove-SqlOrphanUser - -.EXAMPLE -Remove-SqlOrphanUser -SqlServer sql2005 - -Will find and drop all orphan users without matching login of all databases present on server 'sql2005' - -.EXAMPLE -Remove-SqlOrphanUser -SqlServer sqlserver2014a -SqlCredential $cred - -Will find and drop all orphan users without matching login of all databases present on server 'sqlserver2014a'. Will be verified using SQL credentials. - -.EXAMPLE -Remove-SqlOrphanUser -SqlServer sqlserver2014a -Databases db1, db2 -Force - -Will find all and drop orphan users even if exists their matching login on both db1 and db2 databases - -.EXAMPLE -Remove-SqlOrphanUser -SqlServer sqlserver2014a -Users OrphanUser - -Will remove from all databases the user OrphanUser only if not have their matching login - -.EXAMPLE -Remove-SqlOrphanUser -SqlServer sqlserver2014a -Users OrphanUser -Force - -Will remove from all databases the user OrphanUser EVEN if exists their matching login. First will change any schema that it owns to 'dbo'. - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [object]$SqlCredential, - [parameter(Mandatory = $false, ValueFromPipeline = $true)] - [object[]]$Users, - [switch]$Force - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabases -SqlServer $SqlServer -SqlCredential $SqlCredential } } - - BEGIN - { - } - - PROCESS - { - - foreach ($Instance in $SqlServer) - { - Write-Verbose "Attempting to connect to $Instance" - try - { - $server = Connect-SqlServer -SqlServer $Instance -SqlCredential $SqlCredential - } - catch - { - Write-Warning "Can't connect to $Instance or access denied. Skipping." - continue - } - - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - - if ($databases.Count -eq 0) - { - $databases = $server.Databases | Where-Object {$_.IsSystemObject -eq $false -and $_.IsAccessible -eq $true} - } - else - { - if ($pipedatabase.Length -gt 0) - { - $Source = $pipedatabase[0].parent.name - $databases = $pipedatabase.name - } - else - { - $databases = $server.Databases | Where-Object {$_.IsSystemObject -eq $false -and $_.IsAccessible -eq $true -and ($databases -contains $_.Name)} - } - } - - - $CallStack = Get-PSCallStack | Select-Object -Property * - if ($CallStack.Count -eq 1) - { - $StackSource = $CallStack[0].Command - } - else - { - #-2 because index base is 0 and we want the one before the last (the last is the actual command) - $StackSource = $CallStack[($CallStack.Count - 2)].Command - } - - if ($databases.Count -gt 0) - { - $start = [System.Diagnostics.Stopwatch]::StartNew() - - foreach ($db in $databases) - { - try - { - #if SQL 2012 or higher only validate databases with ContainmentType = NONE - if ($server.versionMajor -gt 10) - { - if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) - { - Write-Warning "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." - Continue - } - } - - if ($StackSource -eq "Repair-SqlOrphanUser") - { - Write-Verbose "Call origin: Repair-SqlOrphanUser" - #Will use collection from parameter ($Users) - } - else - { - Write-Verbose "Validating users on database '$($db.Name)'" - - if ($Users.Count -eq 0) - { - #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 - $Users = $db.Users | Where-Object {$_.Login -eq "" -and ($_.ID -gt 4) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false)} - } - else - { - if ($pipedatabase.Length -gt 0) - { - $Source = $pipedatabase[0].parent.name - $Users = $pipedatabase.name - } - else - { - #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 - $Users = $db.Users | Where-Object {$_.Login -eq "" -and ($_.ID -gt 4) -and ($Users -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false)} - } - } - } - - if ($Users.Count -gt 0) - { - Write-Verbose "Orphan users found" - foreach ($User in $Users) - { - $SkipUser = $false - - $ExistLogin = $null - - if ($StackSource -ne "Repair-SqlOrphanUser") - { - #Need to validate Existing Login because the call does not came from Repair-SqlOrphanUser - $ExistLogin = $server.logins | Where-Object {$_.Isdisabled -eq $False -and - $_.IsSystemObject -eq $False -and - $_.IsLocked -eq $False -and - $_.Name -eq $User.Name } - } - - #Schemas only appears on SQL Server 2005 (v9.0) - if ($server.versionMajor -gt 8) - { - - #Validate if user owns any schema - $Schemas = @() - - $Schemas = $db.Schemas | Where-Object {$_.Owner -eq $User.Name} - - if(@($Schemas).Count -gt 0) - { - Write-Verbose "User '$($User.Name)' owns one or more schemas." - - $AlterSchemaOwner = "" - $DropSchema = "" - - foreach ($sch in $Schemas) - { - $NumberObjects = ($db.EnumObjects() | Where-Object {$_.Schema -eq $sch.Name} | Measure-Object).Count - - if ($NumberObjects -gt 0) - { - if ($Force) - { - Write-Verbose "Parameter -Force was used! The schema '$($sch.Name)' have $NumberObjects underlying objects. We will change schema owner to 'dbo' and drop the user." - - if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'. -Force used.")) - { - $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" - - [pscustomobject]@{ - Instance = $server.Name - Database = $db.Name - SchemaName = $sch.Name - Action = "ALTER OWNER" - SchemaOwnerBefore = $sch.Owner - SchemaOwnerAfter = "dbo" - } - } - } - else - { - Write-Warning "Schema '$($sch.Name)' owned by user $($User.Name) have $NumberObjects underlying objects. If you want to change the schemas' owner to 'dbo' and drop the user anyway, use -Force parameter. Skipping user '$USer'" - $SkipUser = $true - break - } - } - else - { - if ($sch.Name -eq $User.Name) - { - Write-Verbose "The schema '$($sch.Name)' have the same name as user '$($User.Name)'. Schema will be dropped." - - if ($Pscmdlet.ShouldProcess($db.Name, "Dropping schema '$($sch.Name)'.")) - { - $DropSchema += "DROP SCHEMA [$($sch.Name)]" - - [pscustomobject]@{ - Instance = $server.Name - Database = $db.Name - SchemaName = $sch.Name - Action = "DROP" - SchemaOwnerBefore = $sch.Owner - SchemaOwnerAfter = "N/A" - } - } - } - else - { - Write-Warning "Schema '$($sch.Name)' does not have any underlying object. Ownership will be changed to 'dbo' so the user can be dropped. Remember to re-check permissions on this schema!" - - if ($Pscmdlet.ShouldProcess($db.Name, "Changing schema '$($sch.Name)' owner to 'dbo'.")) - { - $AlterSchemaOwner += "ALTER AUTHORIZATION ON SCHEMA::[$($sch.Name)] TO [dbo]`r`n" - - [pscustomobject]@{ - Instance = $server.Name - Database = $db.Name - SchemaName = $sch.Name - Action = "ALTER OWNER" - SchemaOwnerBefore = $sch.Owner - SchemaOwnerAfter = "dbo" - } - } - } - } - } - - } - else - { - Write-Verbose "User '$($User.Name)' does not own any schema. Will be dropped." - } - - $query = "$AlterSchemaOwner `r`n$DropSchema `r`nDROP USER " + $User - - Write-Debug $query - } - else - { - $query = "EXEC master.dbo.sp_droplogin @loginame = N'$User'" - } - - if ($ExistLogin) - { - if (!$SkipUser) - { - if ($Force) - { - if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user '$($User.Name)' using -Force")) - { - $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null - Write-Output "User '$($User.Name)' was dropped from $($db.Name). -Force parameter was used!" - } - } - else - { - Write-Warning "Orphan user $($User.Name) has a matching login. The user will not be dropped. If you want to drop anyway, use -Force parameter." - Continue - } - } - } - else - { - if (!$SkipUser) - { - if ($Pscmdlet.ShouldProcess($db.Name, "Dropping user '$($User.Name)'")) - { - $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null - Write-Output "User '$($User.Name)' was dropped from $($db.Name)." - } - } - } - } - } - else - { - Write-Verbose "No orphan users found on database '$($db.Name)'." - } - #reset collection - $Users = $null - } - catch - { - Write-Error $_ - } - } - } - else - { - Write-Verbose "There are no databases to analyse." - } - } - } - - END - { - $server.ConnectionContext.Disconnect() - - $totaltime = ($start.Elapsed) - - #If the call don't come from Repair-SqlOrphanUser function, show elapsed time - if ($StackSource -ne "Repair-SqlOrphanUser") - { - Write-Verbose "Total Elapsed time: $totaltime" - } - } -} diff --git a/functions/Rename-DbaDatabase.ps1 b/functions/Rename-DbaDatabase.ps1 new file mode 100644 index 000000000000..da6de34a1ccd --- /dev/null +++ b/functions/Rename-DbaDatabase.ps1 @@ -0,0 +1,878 @@ +function Rename-DbaDatabase { + <# + .SYNOPSIS + Changes database name, logical file names, file group names and physical file names (optionally handling the move). BETA VERSION. + + .DESCRIPTION + Can change every database metadata that can be renamed. + The ultimate goal is choosing to have a default template to enforce in your environment + so your naming convention for every bit can be put in place in no time. + The process is as follows (it follows the hierarchy of the entities): + - database name is changed (optionally, forcing users out) + - filegroup name(s) are changed accordingly + - logical name(s) are changed accordingly + - physical file(s) are changed accordingly + - if Move is specified, the database will be taken offline and the move will initiate, then it will be taken online + - if Move is not specified, the database remains online (unless SetOffline), and you are in charge of moving files + If any of the above fails, the process stops. + Please take a backup of your databases BEFORE using this, and remember to backup AFTER (also a FULL backup of master) + + It returns an object for each database with all the renames done, plus hidden properties showing a "human" representation of them. + + It's better you store the resulting object in a variable so you can inspect it in case of issues, e.g. "$result = Rename-DbaDatabase ....." + + To get a grasp without worrying of what would happen under the hood, use "Rename-DbaDatabase .... -Preview | Select-Object *" + + .PARAMETER SqlInstance + Target any number of instances, in order to return their build state. + + .PARAMETER SqlCredential + When connecting to an instance, use the credentials specified. + + .PARAMETER Database + Targets only specified databases + + .PARAMETER ExcludeDatabase + Excludes only specified databases + + .PARAMETER AllDatabases + If you want to apply the naming convention system wide, you need to pass this parameter + + .PARAMETER DatabaseName + Pass a template to rename the database name. Valid placeholders are: + - current database name + - date (yyyyMMdd) + + .PARAMETER FileGroupName + Pass a template to rename file group name. Valid placeholders are: + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER LogicalName + Pass a template to rename logical name. Valid placeholders are: + - file type (ROWS, LOG) + - current logical name + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER FileName + Pass a template to rename file name. Valid placeholders are: + - current file name (the basename, without directory nor extension) + - file type (ROWS, LOG, MMO, FS) + - current logical name + - current filegroup name + - current database name + - date (yyyyMMdd) + If distinct names cannot be generated, a counter will be appended (0001, 0002, 0003, etc) + + .PARAMETER ReplaceBefore + If you pass this switch, all upper level "current names" will be inspected and replaced BEFORE doing the + rename according to the template in the current level (remember the hierarchy): + Let's say you have a database named "dbatools_HR", composed by 3 files + - dbatools_HR_Data.mdf + - dbatools_HR_Index.ndf + - dbatools_HR_log.ldf + Rename-DbaDatabase .... -Database "dbatools_HR" -DatabaseName "dbatools_HRARCHIVE" -FileName '' + would end up with this logic: + - database --> no placeholders specified + - dbatools_HR to dbatools_HRARCHIVE + - filenames placeholders specified + --> current database name + current filename" + - dbatools_HR_Data.mdf to dbatools_HRARCHIVEdbatools_HR_Data.mdf + - dbatools_HR_Index.mdf to dbatools_HRARCHIVEdbatools_HR_Data.mdf + - dbatools_HR_log.ldf to dbatools_HRARCHIVEdbatools_HR_log.ldf + Passing this switch, instead, e.g. + Rename-DbaDatabase .... -Database "dbatools_HR" -DatabaseName "dbatools_HRARCHIVE" -FileName '' -ReplaceBefore + end up with this logic instead: + - database --> no placeholders specified + - dbatools_HR to dbatools_HRARCHIVE + - filenames placeholders specified, + , plus -ReplaceBefore --> current database name + replace OLD "upper level" names inside the current filename + - dbatools_HR_Data.mdf to dbatools_HRARCHIVE_Data.mdf + - dbatools_HR_Index.mdf to dbatools_HRARCHIVE_Data.mdf + - dbatools_HR_log.ldf to dbatools_HRARCHIVE_log.ldf + + .PARAMETER Force + Kills any open session to be able to do renames. + + .PARAMETER SetOffline + Kills any open session and sets the database offline to be able to move files + + .PARAMETER Move + If you want this function to move files, else you're the one in charge of it. + This enables the same functionality as SetOffline, killing open transactions and putting the database + offline, then do the actual rename and setting it online again afterwards + + .PARAMETER Preview + Shows the renames without performing any operation (recommended to find your way around this function parameters ;-) ) + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER InputObject + Accepts piped database objects + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Rename + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Rename-DbaDatabase + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName HR2 | select * + + Shows the detailed resultset you'll get renaming the HR database to HR2 without doing anything + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName HR2 + + Renames the HR database to HR2 + + .EXAMPLE + Get-DbaDatabase -SqlInstance sqlserver2014a -Database HR | Rename-DbaDatabase -DatabaseName HR2 + + Same as before, but with a piped database (renames the HR database to HR2) + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" + + Renames the HR database to dbatools_HR + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools__" + + Renames the HR database to dbatools_HR_20170807 (if today is 07th Aug 2017) + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -FileGroupName "dbatools_" + + Renames every FileGroup within HR to "dbatools_[the original FileGroup name]" + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileGroupName "_" + + Renames the HR database to "dbatools_HR", then renames every FileGroup within to "dbatools_HR_[the original FileGroup name]" + Note the "default recursive behaviour" here: for all intents and purposes the result of the former can be obtained with two distinct calls: + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -FileGroupName "dbatools__" + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db stays online (watch out!). You can then proceed manually to move/copy files by hand, set the db offline and then online again to finish the rename process + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" -SetOffline + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db is then set offline (watch out!). You can then proceed manually to move/copy files by hand and then set it online again to finish the rename process + + .EXAMPLE + Rename-DbaDatabase -SqlInstance sqlserver2014a -Database HR -DatabaseName "dbatools_" -FileName "__" -Move + + Renames the HR database to "dbatools_HR" and then all filenames as "dbatools_HR_[Name of the FileGroup]_[original_filename]" + The db is then set offline (watch out!). The function tries to do a simple rename and then sets the db online again to finish the rename process + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory, ParameterSetName = "Server")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [parameter(ParameterSetName = "Server")] + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [string]$DatabaseName, + [string]$FileGroupName, + [string]$LogicalName, + [string]$FileName, + [switch]$ReplaceBefore, + [switch]$Force, + [switch]$Move, + [switch]$SetOffline, + [switch]$Preview, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Pipe")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $CurrentDate = Get-Date -Format 'yyyyMMdd' + + function Get-DbaNameStructure($database) { + $obj = @() + # db name + $obj += "- Database : $database" + # FileGroups + foreach ($fg in $database.FileGroups) { + $obj += " - FileGroup: $($fg.Name)" + # LogicalNames + foreach ($ln in $fg.Files) { + $obj += " - Logical: $($ln.Name)" + $obj += " - FileName: $($ln.FileName)" + } + } + $obj += " - Logfiles" + foreach ($log in $database.LogFiles) { + $obj += " - Logical: $($log.Name)" + $obj += " - FileName: $($log.FileName)" + } + return $obj -Join "`n" + } + + + function Get-DbaKeyByValue($hashtable, $Value) { + ($hashtable.GetEnumerator() | Where-Object Value -eq $Value).Name + } + + if ((Test-Bound -ParameterName SetOffline) -and (-not(Test-Bound -ParameterName FileName))) { + Stop-Function -Category InvalidArgument -Message "-SetOffline is only useful when -FileName is passed. Quitting." + } + } + process { + if (Test-FunctionInterrupt) { return } + if (!$Database -and !$AllDatabases -and !$InputObject -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify a -AllDatabases or -Database/ExcludeDatabase to continue" + return + } + if (!$DatabaseName -and !$FileGroupName -and !$LogicalName -and !$FileName) { + Stop-Function -Message "You must specify at least one of -DatabaseName,-FileGroupName,-LogicalName or -Filename to continue" + return + } + $dbs = @() + if ($InputObject) { + if ($InputObject.Name) { + # comes from Get-DbaDatabase + $dbs += $InputObject + } + } + else { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $all_dbs = $server.Databases | Where-Object IsAccessible + $dbs += $all_dbs | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + } + + # holds all dbs per instance to avoid naming clashes + $InstanceDbs = @{} + + # holds all db file enumerations (used for -Move only) + $InstanceFiles = @{} + + #region db loop + foreach ($db in $dbs) { + # used to stop futher operations on database + $failed = $false + + # pending renames initialized at db level + $Pending_Renames = @() + + $Entities_Before = @{} + + $server = $db.Parent + if ($db.Name -in @('master', 'model', 'msdb', 'tempdb', 'distribution')) { + Write-Message -Level Warning -Message "Database $($db.Name) is a system one, skipping..." + continue + } + if (!$db.IsAccessible) { + Write-Message -Level Warning -Message "Database $($db.Name) is not accessible, skipping..." + continue + } + if ($db.IsMirroringEnabled -eq $true -or $db.AvailabilityGroupName.Length -gt 0) { + Write-Message -Level Warning -Message "Database $($db.Name) is either mirrored or in an AG, skipping..." + continue + } + $Server_Id = $server.DomainInstanceName + if ( !$InstanceDbs.ContainsKey($Server_Id) ) { + $InstanceDbs[$Server_Id] = @{} + foreach ($dn in $server.Databases.Name) { + $InstanceDbs[$Server_Id][$dn] = 1 + } + } + + $Entities_Before['DBN'] = @{} + $Entities_Before['FGN'] = @{} + $Entities_Before['LGN'] = @{} + $Entities_Before['FNN'] = @{} + $Entities_Before['DBN'][$db.Name] = $db.Name + #region databasename + if ($DatabaseName) { + $Orig_DBName = $db.Name + # fixed replacements + $NewDBName = $DatabaseName.Replace('', $Orig_DBName).Replace('', $CurrentDate) + if ($Orig_DBName -eq $NewDBName) { + Write-Message -Level VeryVerbose -Message "Database name unchanged, skipping" + } + else { + if ($InstanceDbs[$Server_Id].ContainsKey($NewDBName)) { + Write-Message -Level Warning -Message "Database $NewDBName exists already, skipping this rename" + $failed = $true + } + else { + if ($PSCmdlet.ShouldProcess($db, "Renaming Database $db to $NewDBName")) { + if ($Force) { + $server.KillAllProcesses($Orig_DBName) + } + try { + if (!$Preview) { + $db.Rename($NewDBName) + } + $InstanceDbs[$Server_Id].Remove($Orig_DBName) + $InstanceDbs[$Server_Id][$NewDBName] = 1 + $Entities_Before['DBN'][$Orig_DBName] = $NewDBName + #$db.Refresh() + } + catch { + Stop-Function -Message "Failed to rename Database : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + } + } + } + } + } + #endregion databasename + #region filegroupname + if ($ReplaceBefore) { + #backfill PRIMARY + $Entities_Before['FGN']['PRIMARY'] = 'PRIMARY' + foreach ($fg in $db.FileGroups.Name) { + $Entities_Before['FGN'][$fg] = $fg + } + } + + if (!$failed -and $FileGroupName) { + $Editable_FGs = $db.FileGroups | Where-Object Name -ne 'PRIMARY' + $New_FGNames = @{} + foreach ($fg in $db.FileGroups.Name) { + $New_FGNames[$fg] = 1 + } + $FGCounter = 0 + foreach ($fg in $Editable_FGs) { + $Orig_FGName = $fg.Name + $Orig_Placeholder = $Orig_FGName + if ($ReplaceBefore) { + # at Filegroup level, we need to worry about database name + $Orig_Placeholder = $Orig_Placeholder.Replace($Entities_Before['DBN'][$Orig_DBName], '') + } + $NewFGName = $FileGroupName.Replace('', $Entities_Before['DBN'][$db.Name]).Replace('', $CurrentDate).Replace('', $Orig_Placeholder) + $FinalFGName = $NewFGName + while ($fg.Name -ne $FinalFGName) { + if ($FinalFGName -in $New_FGNames.Keys) { + $FGCounter += 1 + $FinalFGName = "$NewFGName$($FGCounter.ToString('000'))" + } + else { + break + } + } + if ($fg.Name -eq $FinalFGName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for FileGroup $($fg.Name) (on $db)" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming FileGroup $($fg.Name) to $FinalFGName")) { + try { + if (!$Preview) { + $fg.Rename($FinalFGName) + } + $New_FGNames.Remove($Orig_FGName) + $New_FGNames[$FinalFGName] = 1 + $Entities_Before['FGN'][$Orig_FGName] = $FinalFGName + } + catch { + Stop-Function -Message "Failed to rename FileGroup : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + #$db.FileGroups.Refresh() + } + + #endregion filegroupname + #region logicalname + if ($ReplaceBefore) { + foreach ($fn in $db.FileGroups.Files.Name) { + $Entities_Before['LGN'][$fn] = $fn + } + foreach ($fn in $db.Logfiles.Name) { + $Entities_Before['LGN'][$fn] = $fn + } + } + if (!$failed -and $LogicalName) { + $New_LogicalNames = @{} + foreach ($fn in $db.FileGroups.Files.Name) { + $New_LogicalNames[$fn] = 1 + } + foreach ($fn in $db.Logfiles.Name) { + $New_LogicalNames[$fn] = 1 + } + $LNCounter = 0 + foreach ($fg in $db.FileGroups) { + $logicalfiles = @($fg.Files) + for ($i = 0; $i -lt $logicalfiles.Count; $i++) { + $logical = $logicalfiles[$i] + $FileType = switch ($fg.FileGroupType) { + 'RowsFileGroup' { 'ROWS' } + 'MemoryOptimizedDataFileGroup' { 'MMO' } + 'FileStreamDataFileGroup' { 'FS' } + default { 'STD' } + } + $Orig_LGName = $logical.Name + $Orig_Placeholder = $Orig_LGName + if ($ReplaceBefore) { + # at Logical Name level, we need to worry about database name and filegroup name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '') + } + $NewLGName = $LogicalName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', $fg.Name).Replace( + '', $FileType).Replace('', $Orig_Placeholder) + $FinalLGName = $NewLGName + while ($logical.Name -ne $FinalLGName) { + if ($FinalLGName -in $New_LogicalNames.Keys) { + $LNCounter += 1 + $FinalLGName = "$NewLGName$($LNCounter.ToString('000'))" + } + else { + break + } + } + if ($logical.Name -eq $FinalLGName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for LogicalFile $($logical.Name) (on FileGroup $($fg.Name) (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming LogicalFile $($logical.Name) to $FinalLGName (on FileGroup $($fg.Name))")) { + try { + if (!$Preview) { + $logical.Rename($FinalLGName) + } + $New_LogicalNames.Remove($Orig_LGName) + $New_LogicalNames[$FinalLGName] = 1 + $Entities_Before['LGN'][$Orig_LGName] = $FinalLGName + } + catch { + Stop-Function -Message "Failed to Rename Logical File : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + } + #$fg.Files.Refresh() + if (!$failed) { + $logfiles = @($db.LogFiles) + for ($i = 0; $i -lt $logfiles.Count; $i++) { + $logicallog = $logfiles[$i] + $Orig_LGName = $logicallog.Name + $Orig_Placeholder = $Orig_LGName + if ($ReplaceBefore) { + # at Logical Name level, we need to worry about database name and filegroup name, but for logfiles filegroup is not there + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '') + } + $NewLGName = $LogicalName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', '').Replace( + '', 'LOG').Replace('', $Orig_Placeholder) + $FinalLGName = $NewLGName + if ($FinalLGName.Length -eq 0) { + #someone passed in -LogicalName ''.... but we don't have FGN here + $FinalLGName = $Orig_LGName + } + while ($logicallog.Name -ne $FinalLGName) { + if ($FinalLGName -in $New_LogicalNames.Keys) { + $LNCounter += 1 + $FinalLGName = "$NewLGName$($LNCounter.ToString('000'))" + } + else { + break + } + } + if ($logicallog.Name -eq $FinalLGName) { + Write-Message -Level VeryVerbose -Message "No Rename necessary for LogicalFile log $($logicallog.Name) (LOG on (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming LogicalFile log $($logicallog.Name) to $FinalLGName (LOG)")) { + try { + if (!$Preview) { + $logicallog.Rename($FinalLGName) + } + $New_LogicalNames.Remove($Orig_LGName) + $New_LogicalNames[$FinalLGName] = 1 + $Entities_Before['LGN'][$Orig_LGName] = $FinalLGName + } + catch { + Stop-Function -Message "Failed to Rename Logical File : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + #$db.Logfiles.Refresh() + } + } + #endregion logicalname + #region filename + if ($ReplaceBefore) { + foreach ($fn in $db.FileGroups.Files.FileName) { + $Entities_Before['FNN'][$fn] = $fn + } + foreach ($fn in $db.Logfiles.FileName) { + $Entities_Before['FNN'][$fn] = $fn + } + } + if (!$failed -and $FileName) { + + $New_FileNames = @{} + foreach ($fn in $db.FileGroups.Files.FileName) { + $New_FileNames[$fn] = 1 + } + foreach ($fn in $db.Logfiles.FileName) { + $New_FileNames[$fn] = 1 + } + # we need to inspect what files are in the same directory + # to avoid failing the process because the move won't work + # here we have a dict keyed by instance and then keyed by path + if ( !$InstanceFiles.ContainsKey($Server_Id) ) { + $InstanceFiles[$Server_Id] = @{} + } + foreach ($fn in $New_FileNames.Keys) { + $dirname = [IO.Path]::GetDirectoryName($fn) + if ( !$InstanceFiles[$Server_Id].ContainsKey($dirname) ) { + $InstanceFiles[$Server_Id][$dirname] = @{} + try { + $dirfiles = Get-DbaFile -SqlInstance $server -Path $dirname -EnableException + } + catch { + Write-Message -Level Warning -Message "Failed to enumerate existing files at $dirname, move could go wrong" + } + foreach ($f in $dirfiles) { + $InstanceFiles[$Server_Id][$dirname][$f.Filename] = 1 + } + } + } + $FNCounter = 0 + foreach ($fg in $db.FileGroups) { + $FG_Files = @($fg.Files) + foreach ($logical in $FG_Files) { + $FileType = switch ($fg.FileGroupType) { + 'RowsFileGroup' { 'ROWS' } + 'MemoryOptimizedDataFileGroup' { 'MMO' } + 'FileStreamDataFileGroup' { 'FS' } + default { 'STD' } + } + $FNName = $logical.FileName + $FNNameDir = [IO.Path]::GetDirectoryName($FNName) + $Orig_FNNameLeaf = [IO.Path]::GetFileNameWithoutExtension($logical.FileName) + $Orig_Placeholder = $Orig_FNNameLeaf + if ($ReplaceBefore) { + # at Filename level, we need to worry about database name, filegroup name and logical file name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['LGN'] -Value $logical.Name), '') + } + $NewFNName = $FileName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', $fg.Name).Replace( + '', $FileType).Replace('', $logical.Name).Replace('', $Orig_Placeholder) + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$([IO.Path]::GetExtension($FNName))") + + while ($logical.FileName -ne $FinalFNName) { + if ($InstanceFiles[$Server_Id][$FNNameDir].ContainsKey($FinalFNName)) { + $FNCounter += 1 + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$($FNCounter.ToString('000'))$([IO.Path]::GetExtension($FNName))" + ) + } + else { + break + } + } + if ($logical.FileName -eq $FinalFNName) { + Write-Message -Level VeryVerbose -Message "No rename necessary (on FileGroup $($fg.Name) (on $db))" + continue + } + if ($PSCmdlet.ShouldProcess($db, "Renaming FileName $($logical.FileName) to $FinalFNName (on FileGroup $($fg.Name))")) { + try { + if (!$Preview) { + $logical.FileName = $FinalFNName + $db.Alter() + } + $InstanceFiles[$Server_Id][$FNNameDir].Remove($FNName) + $InstanceFiles[$Server_Id][$FNNameDir][$FinalFNName] = 1 + $Entities_Before['FNN'][$FNName] = $FinalFNName + $Pending_Renames += [pscustomobject]@{ + Source = $FNName + Destination = $FinalFNName + } + } + catch { + Stop-Function -Message "Failed to Rename FileName : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + if (!$failed) { + $FG_Files = @($db.Logfiles) + foreach ($logical in $FG_Files) { + $FNName = $logical.FileName + $FNNameDir = [IO.Path]::GetDirectoryName($FNName) + $Orig_FNNameLeaf = [IO.Path]::GetFileNameWithoutExtension($logical.FileName) + $Orig_Placeholder = $Orig_FNNameLeaf + if ($ReplaceBefore) { + # at Filename level, we need to worry about database name, filegroup name and logical file name + $Orig_Placeholder = $Orig_Placeholder.Replace((Get-DbaKeyByValue -HashTable $Entities_Before['DBN'] -Value $db.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['FGN'] -Value $fg.Name), '').Replace( + (Get-DbaKeyByValue -HashTable $Entities_Before['LGN'] -Value $logical.Name), '') + } + $NewFNName = $FileName.Replace('', $db.Name).Replace('', $CurrentDate).Replace('', '').Replace( + '', 'LOG').Replace('', $logical.Name).Replace('', $Orig_Placeholder) + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$([IO.Path]::GetExtension($FNName))") + while ($logical.FileName -ne $FinalFNName) { + if ($InstanceFiles[$Server_Id][$FNNameDir].ContainsKey($FinalFNName)) { + $FNCounter += 1 + $FinalFNName = [IO.Path]::Combine($FNNameDir, "$NewFNName$($FNCounter.ToString('000'))$([IO.Path]::GetExtension($FNName))") + } + else { + break + } + } + if ($logical.FileName -eq $FinalFNName) { + Write-Message -Level VeryVerbose -Message "No rename necessary for $($logical.FileName) (LOG on (on $db))" + continue + } + + if ($PSCmdlet.ShouldProcess($db, "Renaming FileName $($logical.FileName) to $FinalFNName (LOG)")) { + try { + if (!$Preview) { + $logical.FileName = $FinalFNName + $db.Alter() + } + $InstanceFiles[$Server_Id][$FNNameDir].Remove($FNName) + $InstanceFiles[$Server_Id][$FNNameDir][$FinalFNName] = 1 + $Entities_Before['FNN'][$FNName] = $FinalFNName + $Pending_Renames += [pscustomobject]@{ + Source = $FNName + Destination = $FinalFNName + } + } + catch { + Stop-Function -Message "Failed to Rename FileName : $($_.Exception.InnerException.InnerException.InnerException)" -ErrorRecord $_ -Target $server.DomainInstanceName -OverrideExceptionMessage + # stop any further renames + $failed = $true + break + } + } + } + } + + } + #endregion filename + #region move + $ComputerName = $null + $Final_Renames = New-Object System.Collections.ArrayList + if ([DbaValidate]::IsLocalhost($server.NetName)) { + # locally ran so we can just use rename-item + $ComputerName = $server.NetName + } + else { + # let's start checking if we can access .NetName + $testPS = $false + if ($SqlCredential) { + # why does Test-PSRemoting require a Credential param ? this is ugly... + $testPS = Test-PSRemoting -ComputerName $server.NetName -Credential $SqlCredential -ErrorAction Stop + } + else { + $testPS = Test-PSRemoting -ComputerName $server.NetName -ErrorAction Stop + } + if (!($testPS)) { + # let's try to resolve it to a more qualified name, without "cutting" knowledge about the domain (only $server.Name possibly holds the complete info) + $Resolved = (Resolve-DbaNetworkName -ComputerName $server.Name).FullComputerName + if ($SqlCredential) { + $testPS = Test-PSRemoting -ComputerName $Resolved -Credential $SqlCredential -ErrorAction Stop + } + else { + $testPS = Test-PSRemoting -ComputerName $Resolved -ErrorAction Stop + } + if ($testPS) { + $ComputerName = $Resolved + } + } + else { + $ComputerName = $server.NetName + } + } + foreach ($op in $pending_renames) { + if ([DbaValidate]::IsLocalhost($server.NetName)) { + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + else { + if ($null -eq $ComputerName) { + # if we don't have remote access ($ComputerName is null) we can fallback to admin shares if they're available + if (Test-Path (Join-AdminUnc -ServerName $server.NetName -filepath $op.Source)) { + $null = $Final_Renames.Add([pscustomobject]@{ + Source = Join-AdminUnc -ServerName $server.NetName -filepath $op.Source + Destination = Join-AdminUnc -ServerName $server.NetName -filepath $op.Destination + ComputerName = $server.NetName + }) + } + else { + # flag the impossible rename ($ComputerName is $null) + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + } + else { + # we can do renames in a remote pssession + $null = $Final_Renames.Add([pscustomobject]@{ + Source = $op.Source + Destination = $op.Destination + ComputerName = $ComputerName + }) + } + } + } + $Status = 'FULL' + if (!$failed -and ($SetOffline -or $Move) -and $Final_Renames) { + if (!$Move) { + Write-Message -Level VeryVerbose -Message "Setting the database offline. You are in charge of moving the files to the new location" + # because renames still need to be dealt with + $Status = 'PARTIAL' + } + else { + if ($PSCmdlet.ShouldProcess($db, "File Rename required, setting db offline")) { + $SetState = Set-DbaDatabaseState -SqlInstance $server -Database $db.Name -Offline -Force + if ($SetState.Status -ne 'OFFLINE') { + Write-Message -Level Warning -Message "Setting db offline failed, You are in charge of moving the files to the new location" + # because it was impossible to set the database offline + $Status = 'PARTIAL' + } + else { + try { + while ($Final_Renames.Count -gt 0) { + $op = $Final_Renames.Item(0) + if ($null -eq $op.ComputerName) { + Stop-Function -Message "No access to physical files for renames" + } + else { + Write-Message -Level VeryVerbose -Message "Moving file $($op.Source) to $($op.Destination)" + if (!$Preview) { + $scriptblock = { + $op = $args[0] + Rename-Item -Path $op.Source -NewName $op.Destination + } + Invoke-Command2 -ComputerName $op.ComputerName -Credential $sqlCredential -ScriptBlock $scriptblock -ArgumentList $op + } + } + $null = $Final_Renames.RemoveAt(0) + } + } + catch { + $failed = $true + # because a rename operation failed + $Status = 'PARTIAL' + Stop-Function -Message "Failed to rename $($op.Source) to $($op.Destination), you are in charge of moving the files to the new location" -ErrorRecord $_ -Target $instance -Exception $_.Exception -Continue + } + if (!$failed) { + if ($PSCmdlet.ShouldProcess($db, "Setting database online")) { + $SetState = Set-DbaDatabaseState -SqlInstance $server -Database $db.Name -Online -Force + if ($SetState.Status -ne 'ONLINE') { + Write-Message -Level Warning -Message "Setting db online failed" + # because renames were done, but the database didn't wake up + $Status = 'PARTIAL' + } + else { + $Status = 'FULL' + } + } + } + } + } + } + } + else { + # because of a previous error with renames to do + $Status = 'PARTIAL' + } + } + else { + if (!$failed) { + # because no previous error and not filename + $Status = 'FULL' + } + else { + # because previous errors and not filename + $Status = 'PARTIAL' + } + } + #endregion move + # remove entities that match for the output + foreach ($k in $Entities_Before.Keys) { + $ToRemove = $Entities_Before[$k].GetEnumerator() | Where-Object { $_.Name -eq $_.Value } | Select-Object -ExpandProperty Name + foreach ($el in $ToRemove) { + $Entities_Before[$k].Remove($el) + } + } + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + DBN = $Entities_Before['DBN'] + DatabaseRenames = ($Entities_Before['DBN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + FGN = $Entities_Before['FGN'] + FileGroupsRenames = ($Entities_Before['FGN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + LGN = $Entities_Before['LGN'] + LogicalNameRenames = ($Entities_Before['LGN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + FNN = $Entities_Before['FNN'] + FileNameRenames = ($Entities_Before['FNN'].GetEnumerator() | Foreach-Object { "$($_.Name) --> $($_.Value)" }) -Join "`n" + PendingRenames = $Final_Renames + Status = $Status + } | Select-DefaultView -ExcludeProperty DatabaseRenames, FileGroupsRenames, LogicalNameRenames, FileNameRenames + } + #endregion db loop + } +} diff --git a/functions/Rename-DbaLogin.ps1 b/functions/Rename-DbaLogin.ps1 index 6b05ae661ad2..27ae77d4cd2d 100644 --- a/functions/Rename-DbaLogin.ps1 +++ b/functions/Rename-DbaLogin.ps1 @@ -1,13 +1,12 @@ -Function Rename-DbaLogin -{ -<# -.SYNOPSIS -Rename-DbaLogin will rename login and database mapping for a specified login. +function Rename-DbaLogin { + <# +.SYNOPSIS +Rename-DbaLogin will rename login and database mapping for a specified login. .DESCRIPTION -There are times where you might want to rename a login that was copied down, or if the name is not descriptive for what it does. +There are times where you might want to rename a login that was copied down, or if the name is not descriptive for what it does. -It can be a pain to update all of the mappings for a spefic user, this does it for you. +It can be a pain to update all of the mappings for a specific user, this does it for you. .PARAMETER SqlInstance Source SQL Server.You must have sysadmin access and server version must be SQL Server version 2000 or greater. @@ -15,148 +14,143 @@ Source SQL Server.You must have sysadmin access and server version must be SQL S .PARAMETER Destination Destination Sql Server. You must have sysadmin access and server version must be SQL Server version 2000 or greater. -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) -$scred = Get-Credential, then pass $scred object to the -SourceSqlCredential parameter. +.PARAMETER Login +The current Login on the server - this list is auto-populated from the server. -Windows Authentication will be used if DestinationSqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. -To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER Login -The current Login on the server - -.PARAMETER NewLogin -The new Login that you wish to use. If it is a windows user login, then the SID must match. +.PARAMETER NewLogin +The new Login that you wish to use. If it is a windows user login, then the SID must match. .PARAMETER Confirm Prompts to confirm actions - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. -.NOTES -Original Author: Mitchell Hamann (@SirCaptainMitch) +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire +.PARAMETER EnableException +By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. +This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. +Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +.NOTES +Tags: Login +Author: Mitchell Hamann (@SirCaptainMitch) -You should have received a copy of the GNU General Public License along with this program. If not, see . +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Rename-DbaLogin -.EXAMPLE +.EXAMPLE Rename-DbaLogin -SqlInstance localhost -Login DbaToolsUser -NewLogin captain -SQL Login Example +SQL Login Example -.EXAMPLE +.EXAMPLE Rename-DbaLogin -SqlInstance localhost -Login domain\oldname -NewLogin domain\newname Change the windowsuser login name. -.EXAMPLE +.EXAMPLE Rename-DbaLogin -SqlInstance localhost -Login dbatoolsuser -NewLogin captain -WhatIf WhatIf Example - #> - [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] - param ( - [parameter(Mandatory = $true)] - [object]$SqlInstance, - [System.Management.Automation.PSCredential]$SqlCredential, - [parameter(Mandatory = $true)] - [String]$NewLogin - ) - - DynamicParam { if ($SqlInstance) { return Get-ParamSqlLogin -SqlServer $SqlInstance -SqlCredential $SqlCredential } } - - BEGIN - { - $Login = $psboundparameters.Login - - if (!$Login) { throw "You must specify a login" } - - $server = Connect-SqlServer -SqlServer $SqlInstance -SqlCredential $SqlCredential - $Databases = $server.Databases - - $currentLogin = $server.Logins[$Login] - - } - PROCESS - { - if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing Login name from [$Login] to [$NewLogin]")) - { - try - { - $dbenums = $currentLogin.EnumDatabaseMappings() - $currentLogin.rename($NewLogin) - [pscustomobject]@{ - SqlInstance = $server.name - Database = "N/A" - OldLogin = $Login - NewLogin = $NewLogin - Notes = "Successfully renamed login" - } - } - catch - { - $dbenums = $null - [pscustomobject]@{ - SqlInstance = $server.name - Database = $null - OldLogin = $Login - NewLogin = $NewLogin - Notes = "Failure to rename login" - } - Write-Exception $_ - continue - } - } - - foreach ($db in $dbenums) - { - $db = $databases[$db.DBName] - $user = $db.Users[$Login] - Write-Verbose "Starting update for $db" - - if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing database $db user $user from [$Login] to [$NewLogin]")) - { - try - { - $oldname = $user.name - $user.Rename($NewLogin) - [pscustomobject]@{ - SqlInstance = $server.name - Database = $db.name - OldUser = $oldname - NewUser = $NewLogin - Notes = "Successfully renamed database user" - } - - } - catch - { - Write-Warning "Rolling back update to login: $Login" - $currentLogin.rename($Login) - - [pscustomobject]@{ - SqlInstance = $server.name - Database = $db.name - OldUser = $NewLogin - NewUser = $oldname - Notes = "Failure to rename. Rolled back change." - } - Write-Exception $_ - break - } - } - } - } -} + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [string]$Login, + [parameter(Mandatory = $true)] + [string]$NewLogin, + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $Databases = $server.Databases | Where-Object IsAccessible + $currentLogin = $server.Logins[$Login] + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing Login name from [$Login] to [$NewLogin]")) { + try { + $dbenums = $currentLogin.EnumDatabaseMappings() + $currentLogin.rename($NewLogin) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $null + OldLogin = $Login + NewLogin = $NewLogin + Status = "Successful" + } + } + catch { + $dbenums = $null + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $null + OldLogin = $Login + NewLogin = $NewLogin + Status = "Failure" + } + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $login + } + } + + foreach ($db in $dbenums) { + $db = $databases[$db.DBName] + $user = $db.Users[$Login] + Write-Message -Level Verbose -Message "Starting update for $db" + + if ($Pscmdlet.ShouldProcess($SqlInstance, "Changing database $db user $user from [$Login] to [$NewLogin]")) { + try { + $oldname = $user.name + $user.Rename($NewLogin) + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OldUser = $oldname + NewUser = $NewLogin + Status = "Successful" + } + + } + catch { + Write-Message -Level Warning -Message "Rolling back update to login: $Login" + $currentLogin.rename($Login) + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.name + OldUser = $NewLogin + NewUser = $oldname + Status = "Failure to rename. Rolled back change." + } + Stop-Function -Message "Failure" -ErrorRecord $_ -Target $NewLogin + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Repair-DbaOrphanUser.ps1 b/functions/Repair-DbaOrphanUser.ps1 new file mode 100644 index 000000000000..fa493978aa2a --- /dev/null +++ b/functions/Repair-DbaOrphanUser.ps1 @@ -0,0 +1,241 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Repair-DbaOrphanUser { + <# + .SYNOPSIS + Finds orphan users with existing login and remaps them. + + .DESCRIPTION + An orphan user is defined by a user that does not have a matching login (Login property = ""). + + If the matching login exists it must be: + Enabled + Not a system object + Not locked + Have the same name that user + + You can drop users that does not have their matching login by specifying the parameter -RemoveNotExisting. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server + + .PARAMETER Users + Specifies the list of usernames to repair. + + .PARAMETER Force + Forces alter schema to dbo owner so users can be dropped. + + .PARAMETER RemoveNotExisting + If this switch is enabled, all users that do not have a matching login will be dropped from the database. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sql2005 + + Finds and repairs all orphan users of all databases present on server 'sql2005' + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -SqlCredential $cred + + Finds and repair all orphan users in all databases present on server 'sqlserver2014a'. SQL credentials are used to authenticate to the server. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1, db2 + + Finds and repairs all orphan users in both db1 and db2 databases. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Database db1 -Users OrphanUser + + Finds and repairs user 'OrphanUser' in 'db1' database. + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -Users OrphanUser + + Finds and repairs user 'OrphanUser' on all databases + + .EXAMPLE + Repair-DbaOrphanUser -SqlInstance sqlserver2014a -RemoveNotExisting + + Finds all orphan users of all databases present on server 'sqlserver2014a'. Removes all users that do not have matching Logins. + + .NOTES + Tags: Orphan + Author: Claudio Silva (@ClaudioESSilva) + Editor: Simone Bizzotto (@niphlod) + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Repair-DbaOrphanUser + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [parameter(Mandatory = $false, ValueFromPipeline = $true)] + [object[]]$Users, + [switch]$RemoveNotExisting, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance." + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $SqlInstance." + continue + } + + $DatabaseCollection = $server.Databases | Where-Object IsAccessible + + if ($Database) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -In $Database + } + if ($ExcludeDatabase) { + $DatabaseCollection = $DatabaseCollection | Where-Object Name -NotIn $ExcludeDatabase + } + + if ($DatabaseCollection.Count -gt 0) { + foreach ($db in $DatabaseCollection) { + try { + #if SQL 2012 or higher only validate databases with ContainmentType = NONE + if ($server.versionMajor -gt 10) { + if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) { + Write-Message -Level Warning -Message "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." + Continue + } + } + + Write-Message -Level Verbose -Message "Validating users on database '$db'." + + if ($Users.Count -eq 0) { + #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false } + } + else { + + #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 + $UsersToWork = $db.Users | Where-Object { $_.Login -eq "" -and ($_.ID -gt 4) -and ($Users -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false) } + + } + + if ($UsersToWork.Count -gt 0) { + Write-Message -Level Verbose -Message "Orphan users found" + $UsersToRemove = @() + foreach ($User in $UsersToWork) { + $ExistLogin = $server.logins | Where-Object { + $_.Isdisabled -eq $False -and + $_.IsSystemObject -eq $False -and + $_.IsLocked -eq $False -and + $_.Name -eq $User.Name + } + + if ($ExistLogin) { + if ($server.versionMajor -gt 8) { + $query = "ALTER USER " + $User + " WITH LOGIN = " + $User + } + else { + $query = "exec sp_change_users_login 'update_one', '$User'" + } + + if ($Pscmdlet.ShouldProcess($db.Name, "Mapping user '$($User.Name)'")) { + $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null + Write-Message -Level Verbose -Message "User '$($User.Name)' mapped with their login." + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $User.Name + Status = "Success" + } + } + } + else { + if ($RemoveNotExisting) { + #add user to collection + $UsersToRemove += $User + } + else { + Write-Message -Level Verbose -Message "Orphan user $($User.Name) does not have matching login." + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + User = $User.Name + Status = "No matching login" + } + } + } + } + + #With the collection complete invoke remove. + if ($RemoveNotExisting) { + if ($Force) { + if ($Pscmdlet.ShouldProcess($db.Name, "Remove-DbaOrphanUser")) { + Write-Message -Level Verbose -Message "Calling 'Remove-DbaOrphanUser' with -Force." + Remove-DbaOrphanUser -SqlInstance $server -Database $db.Name -User $UsersToRemove -Force + } + } + else { + if ($Pscmdlet.ShouldProcess($db.Name, "Remove-DbaOrphanUser")) { + Write-Message -Level Verbose -Message "Calling 'Remove-DbaOrphanUser'." + Remove-DbaOrphanUser -SqlInstance $server -Database $db.Name -User $UsersToRemove + } + } + } + } + else { + Write-Message -Level Verbose -Message "No orphan users found on database '$db'." + } + #reset collection + $UsersToWork = $null + } + catch { + Stop-Function -Message $_ -Continue + } + } + } + else { + Write-Message -Level Verbose -Message "There are no databases to analyse." + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Repair-SqlOrphanUser + } +} \ No newline at end of file diff --git a/functions/Repair-DbaServerName.ps1 b/functions/Repair-DbaServerName.ps1 index e8d125c56a33..8d4ef8bfb415 100644 --- a/functions/Repair-DbaServerName.ps1 +++ b/functions/Repair-DbaServerName.ps1 @@ -1,349 +1,264 @@ -Function Repair-DbaServerName -{ -<# -.SYNOPSIS -Renames @@SERVERNAME to match with the Windows name. - -.DESCRIPTION -When a SQL Server's host OS is renamed, the SQL Server should be as well. This helps with Availability Groups and Kerberos. - -This command renames @@SERVERNAME to match with the Windows name. The new name is automatically determined. It does not matter if you use an alias to connect to the SQL instance. - -If the automatically determiend new name matches the old name, the command will not run. - -https://www.mssqltips.com/sqlservertip/2525/steps-to-change-the-server-name-for-a-sql-server-machine/ - -.PARAMETER SqlServer -The SQL Server that you're connecting to. - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user. - -.PARAMETER Detailed -Specifies if the servername is updatable. If updatable -eq $false, it will return the reasons why. - -.PARAMETER Force -By default, this command produces a ton of confirm prompts. Force bypasses many of these confirms, but not all. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER AutoFix -Automatically performs the fix - -.NOTES -Tags: SPN -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Repair-DbaServerName - -.EXAMPLE -Repair-DbaServerName -SqlServer sql2014 - -Checks to see if the server is updatable, prompts galore, changes name. - -.EXAMPLE -Repair-DbaServerName -SqlServer sql2014 -AutoFix - -Even more prompts/confirms, but removes Replication or breaks mirroring if necessary. - -.EXAMPLE -Repair-DbaServerName -SqlServer sql2014 -AutoFix -Force - -Skips some prompts/confirms but not all of them. - -#> - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [string[]]$SqlServer, - [PsCredential]$Credential, - [switch]$AutoFix, - [switch]$Force - ) - - BEGIN - { - if ($Force -eq $true) { $ConfirmPreference = "None" } - $collection = New-Object System.Collections.ArrayList - } - - PROCESS - { - $servercount++ - - foreach ($servername in $SqlServer) - { - try - { - $server = Connect-SqlServer -SqlServer $servername -SqlCredential $Credential - } - catch - { - if ($servercount -eq 1 -and $SqlServer.count -eq 1) - { - throw $_ - } - else - { - Write-Warning "Can't connect to $servername. Moving on." - Continue - } - } - - if ($server.isClustered) - { - if ($servercount -eq 1 -and $SqlServer.count -eq 1) - { - # If we ever decide with a -Force to support a cluster name change - # We would compare $server.NetName, and never ComputerNamePhysicalNetBIOS - throw "$servername is a cluster. Microsoft does not support renaming clusters." - } - else - { - Write-Warning "$servername is a cluster. Microsoft does not support renaming clusters." - Continue - } - } - - if ($server.VersionMajor -eq 8) - { - if ($servercount -eq 1 -and $SqlServer.count -eq 1) - { - throw "SQL Server 2000 not supported." - } - else - { - Write-Warning "SQL Server 2000 not supported. Skipping $servername." - Continue - } - } - - # Check to see if we can easily proceed - Write-Verbose "Executing Test-DbaServerName to see if the server is in a state to be renamed. " - - $nametest = Test-DbaServerName $servername -Detailed -NoWarning - $serverinstancename = $nametest.ServerInstanceName - $sqlservername = $nametest.SqlServerName - - if ($nametest.RenameRequired -eq $false) - { - return "Good news! $serverinstancename's @@SERVERNAME does not need to be changed. If you'd like to rename it, first rename the Windows server." - } - - if ($nametest.updatable -eq $false) - { - Write-Output "Test-DbaServerName reports that the rename cannot proceed with a rename in this $servername's current state." - - $nametest - - foreach ($nametesterror in $nametest.Blockers) - { - if ($nametesterror -like '*replication*') - { - $replication = $true - - if ($AutoFix -eq $false) - { - throw "Cannot proceed because some databases are involved in replication. You can run exec sp_dropdistributor @no_checks = 1 but that may be pretty dangerous. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also break all database mirrors." - } - else - { - if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) - { - $title = "You have chosen to AutoFix the blocker: replication." - $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" - $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" - $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" - $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) - $result = $host.ui.PromptForChoice($title, $message, $options, 1) - - if ($result -eq 1) - { - throw "Cannot continue" - } - else - { - Write-Output "`nPerforming sp_dropdistributor @no_checks = 1" - $sql = "sp_dropdistributor @no_checks = 1" - Write-Debug $sql - try - { - $null = $server.ConnectionContext.ExecuteNonQuery($sql) - Write-Output "Successfully executed $sql`n" - } - catch - { - Write-Exception $_ - throw $_ - } - } - } - } - } - elseif ($Error -like '*mirror*') - { - if ($AutoFix -eq $false) - { - throw "Cannot proceed because some databases are being mirrored. Stop mirroring to proceed. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also stop replication." - } - else - { - if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) - { - $title = "You have chosen to AutoFix the blocker: mirroring." - $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" - $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" - $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" - $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) - $result = $host.ui.PromptForChoice($title, $message, $options, 1) - - if ($result -eq 1) - { - Write-Output "Okay, moving on." - } - else - { - Write-Output "Removing Mirroring" - - foreach ($database in $server.Databases) - { - if ($database.IsMirroringEnabled) - { - $dbname = $database.name - - try - { - Write-Output "Breaking mirror for $dbname" - $database.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) - $database.Alter() - $database.Refresh() - } - catch - { - Write-Exception $_ - throw "Could not break mirror for $dbname. Skipping." - } - } - } - } - } - } - } - } - } - # ^ That's embarassing - - $instancename = $instance = $server.InstanceName - - if ($instancename.length -eq 0) - { - $instancename = $instance = "MSSQLSERVER" - } - - try - { - $allsqlservices = Get-Service -ComputerName $server.ComputerNamePhysicalNetBIOS -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "SQL*$instance*" -and $_.Status -eq "Running" } - } - catch - { - Write-Warning "Can't contact $servername using Get-Service. This means the script will not be able to automatically restart SQL services." - } - - if ($nametest.Warnings.length -gt 0) - { - $reportingservice = Get-Service -ComputerName $server.ComputerNamePhysicalNetBIOS -DisplayName "SQL Server Reporting Services ($instance)" -ErrorAction SilentlyContinue - - if ($reportingservice.Status -eq "Running") - { - if ($Pscmdlet.ShouldProcess($server.name, "Reporting Services is running for this instance. Would you like to automatically stop this service?")) - { - $reportingservice | Stop-Service - Write-Warning "You must reconfigure Reporting Services using Reporting Services Configuration Manager or PowerShell once the server has been successfully renamed." - } - } - } - - if ($Pscmdlet.ShouldProcess($server.name, "Performing sp_dropserver to remove the old server name, $sqlservername, then sp_addserver to add $serverinstancename")) - { - $sql = "sp_dropserver '$sqlservername'" - Write-Debug $sql - try - { - $null = $server.ConnectionContext.ExecuteNonQuery($sql) - Write-Output "`nSuccessfully executed $sql" - } - catch - { - Write-Exception $_ - throw $_ - } - - $sql = "sp_addserver '$serverinstancename', local" - Write-Debug $sql - - try - { - $null = $server.ConnectionContext.ExecuteNonQuery($sql) - Write-Output "Successfully executed $sql" - } - catch - { - Write-Exception $_ - throw $_ - } - $renamed = $true - } - - if ($allsqlservices -eq $null) - { - Write-Warning "Could not contact $($server.ComputerNamePhysicalNetBIOS) using Get-Service. You must manually restart the SQL Server instance." - $needsrestart = $true - } - else - { - if ($Pscmdlet.ShouldProcess($server.ComputerNamePhysicalNetBIOS, "Rename complete! The SQL Service must be restarted to commit the changes. Would you like to restart the $instancname instance now?")) - { - try - { - Write-Output "`nStopping SQL Services for the $instancename instance" - $allsqlservices | Stop-Service -Force -WarningAction SilentlyContinue # because it reports the wrong name - Write-Output "Starting SQL Services for the $instancename instance" - $allsqlservices | Where-Object { $_.DisplayName -notlike "*reporting*" } | Start-Service -WarningAction SilentlyContinue # because it reports the wrong name - } - catch - { - Write-Exception $_ - throw "Could not restart at least one SQL Service :(" - } - } - } - - if ($renamed -eq $true) - { - Write-Output "`n$servername successfully renamed from $sqlservername to $serverinstancename" - } - - if ($needsrestart -eq $true) - { - Write-Output "SQL Service restart for $serverinstancename still required" - } - } - } - END - { - # Nothing needed - } -} +function Repair-DbaServerName { + <# + .SYNOPSIS + Renames @@SERVERNAME to match with the Windows name. + + .DESCRIPTION + When a SQL Server's host OS is renamed, the SQL Server should be as well. This helps with Availability Groups and Kerberos. + + This command renames @@SERVERNAME to match with the Windows name. The new name is automatically determined. It does not matter if you use an alias to connect to the SQL instance. + + If the automatically determined new name matches the old name, the command will not run. + + https://www.mssqltips.com/sqlservertip/2525/steps-to-change-the-server-name-for-a-sql-server-machine/ + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER AutoFix + If this switch is enabled, the repair will be performed automatically. + + .PARAMETER Force + If this switch is enabled, most confirmation prompts will be skipped. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: SPN + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Repair-DbaServerName + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 + + Checks to see if the server name is updatable and changes the name with a number of prompts. + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 -AutoFix + + Checks to see if the server name is updatable and automatically performs the change. Replication or mirroring will be broken if necessary. + + .EXAMPLE + Repair-DbaServerName -SqlInstance sql2014 -AutoFix -Force + + Checks to see if the server name is updatable and automatically performs the change, bypassing most prompts and confirmations. Replication or mirroring will be broken if necessary. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [switch]$AutoFix, + [switch]$Force, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($Force -eq $true) { + $ConfirmPreference = "None" + } + } + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if ($server.isClustered) { + Write-Message -Level Warning -Message "$instance is a cluster. Microsoft does not support renaming clusters." + continue + } + + + # Check to see if we can easily proceed + + $nametest = Test-DbaServerName $server -EnableException | Select-Object * + $oldserverinstancename = $nametest.ServerName + $SqlInstancename = $nametest.SqlInstance + + if ($nametest.RenameRequired -eq $false) { + Stop-Function -Continue -Message "Good news! $oldserverinstancename's @@SERVERNAME does not need to be changed. If you'd like to rename it, first rename the Windows server." + } + + if (-not $nametest.updatable) { + Write-Message -Level Output -Message "Test-DbaServerName reports that the rename cannot proceed with a rename in this $instance's current state." + + foreach ($nametesterror in $nametest.Blockers) { + if ($nametesterror -like '*replication*') { + + if (-not $AutoFix) { + Stop-Function -Message "Cannot proceed because some databases are involved in replication. You can run exec sp_dropdistributor @no_checks = 1 but that may be pretty dangerous. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also break all database mirrors." + return + } + else { + if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) { + $title = "You have chosen to AutoFix the blocker: replication." + $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 1) + + if ($result -eq 1) { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + else { + Write-Message -Level Output -Message "`nPerforming sp_dropdistributor @no_checks = 1." + $sql = "sp_dropdistributor @no_checks = 1" + Write-Message -Level Debug -Message $sql + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + } + } + } + } + elseif ($Error -like '*mirror*') { + if ($AutoFix -eq $false) { + Stop-Function -Message "Cannot proceed because some databases are being mirrored. Stop mirroring to proceed. Alternatively, you can run -AutoFix to automatically fix this issue. AutoFix will also stop replication." -Continue + } + else { + if ($Pscmdlet.ShouldProcess("console", "Prompt will appear for confirmation to break replication.")) { + $title = "You have chosen to AutoFix the blocker: mirroring." + $message = "We can run sp_dropdistributor which will pretty much destroy replication on this server. Do you wish to continue? (Y/N)" + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will continue" + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will exit" + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 1) + + if ($result -eq 1) { + Write-Message -Level Output -Message "Okay, moving on." + } + else { + Write-Message -Level Verbose -Message "Removing Mirroring" + + foreach ($database in $server.Databases) { + if ($database.IsMirroringEnabled) { + $dbname = $database.name + + try { + Write-Message -Level Verbose -Message "Breaking mirror for $dbname." + $database.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $database.Alter() + $database.Refresh() + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + #throw "Could not break mirror for $dbname. Skipping." + } + } + } + } + } + } + } + } + } + # ^ That's embarrassing + + $instancename = $server.InstanceName + + if (-not $instancename) { + $instancename = "MSSQLSERVER" + } + + try { + $allsqlservices = Get-Service -ComputerName $instance.ComputerName -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "SQL*$instancename*" -and $_.Status -eq "Running" } + } + catch { + Write-Message -Level Warning -Message "Can't contact $instance using Get-Service. This means the script will not be able to automatically restart SQL services." + } + + if ($nametest.Warnings.length -gt 0) { + $reportingservice = Get-Service -ComputerName $instance.ComputerName -DisplayName "SQL Server Reporting Services ($instancename)" -ErrorAction SilentlyContinue + + if ($reportingservice.Status -eq "Running") { + if ($Pscmdlet.ShouldProcess($server.name, "Reporting Services is running for this instance. Would you like to automatically stop this service?")) { + $reportingservice | Stop-Service + Write-Message -Level Warning -Message "You must reconfigure Reporting Services using Reporting Services Configuration Manager or PowerShell once the server has been successfully renamed." + } + } + } + + if ($Pscmdlet.ShouldProcess($server.name, "Performing sp_dropserver to remove the old server name, $oldserverinstancename, then sp_addserver to add $SqlInstancename")) { + $sql = "sp_dropserver '$oldserverinstancename'" + Write-Message -Level Debug -Message $sql + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + } + + $sql = "sp_addserver '$SqlInstancename', local" + Write-Message -Level Debug -Message $sql + + try { + $null = $server.Query($sql) + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ + return + } + $renamed = $true + } + + if ($null -eq $allsqlservices) { + Write-Message -Level Warning -Message "Could not contact $($instance.ComputerName) using Get-Service. You must manually restart the SQL Server instance." + $needsrestart = $true + } + else { + if ($Pscmdlet.ShouldProcess($instance.ComputerName, "Rename complete! The SQL Service must be restarted to commit the changes. Would you like to restart the $instancename instance now?")) { + try { + Write-Message -Level Verbose -Message "Stopping SQL Services for the $instancename instance" + $allsqlservices | Stop-Service -Force -WarningAction SilentlyContinue # because it reports the wrong name + Write-Message -Level Verbose -Message "Starting SQL Services for the $instancename instance." + $allsqlservices | Where-Object { $_.DisplayName -notlike "*reporting*" } | Start-Service -WarningAction SilentlyContinue # because it reports the wrong name + } + catch { + Stop-Function -Message "Failure" -Target $server -ErrorRecord $_ -Continue + } + } + } + + if ($renamed -eq $true) { + Write-Message -Level Verbose -Message "$instance successfully renamed from $oldserverinstancename to $SqlInstancename." + Test-DbaServerName -SqlInstance $server + } + + if ($needsrestart -eq $true) { + Write-Message -Level Warning -Message "SQL Service restart for $SqlInstancename still required." + } + } + } +} \ No newline at end of file diff --git a/functions/Repair-SqlOrphanUser.ps1 b/functions/Repair-SqlOrphanUser.ps1 deleted file mode 100644 index a2ff9cd20c00..000000000000 --- a/functions/Repair-SqlOrphanUser.ps1 +++ /dev/null @@ -1,252 +0,0 @@ -Function Repair-SqlOrphanUser -{ -<# -.SYNOPSIS -Find orphan users with existing login and remap. - -.DESCRIPTION -An orphan user is defined by a user that does not have their matching login. (Login property = "") - -If the matching login exists it must be: - Enabled - Not a system object - Not locked - Have the same name that user - -You can drop users that does not have their matching login by especifing the parameter -RemoveNotExisting This will be made by calling Remove-SqlOrphanUser function. - - -.PARAMETER SqlServer -The SQL Server instance. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. - -Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER Users -List of users to repair - -.PARAMETER RemoveNotExisting -If passed, all users that not have their matching login will be dropped from database - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sql2005 - -Will find and repair all orphan users of all databases present on server 'sql2005' - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sqlserver2014a -SqlCredential $cred - -Will find and repair all orphan users of all databases present on server 'sqlserver2014a'. Will be verified using SQL credentials. - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sqlserver2014a -Databases db1, db2 - -Will find and repair all orphan users on both db1 and db2 databases - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sqlserver2014a -Databases db1 -Users OrphanUser - -Will find and repair user 'OrphanUser' on 'db1' database - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sqlserver2014a -Users OrphanUser - -Will find and repair user 'OrphanUser' on all databases - -.EXAMPLE -Repair-SqlOrphanUser -SqlServer sqlserver2014a -RemoveNotExisting - -Will find all orphan users of all databases present on server 'sqlserver2014a' -Will also remove all users that does not have their matching login by calling Remove-SqlOrphanUser function - -.NOTES -Tags: Orphan -Original Author: Claudio Silva (@ClaudioESSilva) -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Repair-SqlOrphanUser -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [object]$SqlCredential, - [parameter(Mandatory = $false, ValueFromPipeline = $true)] - [object[]]$Users, - [switch]$RemoveNotExisting - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabases -SqlServer $SqlServer -SqlCredential $SqlCredential } } - - BEGIN - { - Write-Output "Attempting to connect to Sql Server.." - $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential - } - - PROCESS - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - - if ($databases.Count -eq 0) - { - $databases = $server.Databases | Where-Object {$_.IsSystemObject -eq $false -and $_.IsAccessible -eq $true} - } - else - { - if ($pipedatabase.Length -gt 0) - { - $Source = $pipedatabase[0].parent.name - $databases = $pipedatabase.name - } - else - { - $databases = $server.Databases | Where-Object {$_.IsSystemObject -eq $false -and $_.IsAccessible -eq $true -and ($databases -contains $_.Name)} - } - } - - if ($databases.Count -gt 0) - { - $start = [System.Diagnostics.Stopwatch]::StartNew() - - foreach ($db in $databases) - { - try - { - #if SQL 2012 or higher only validate databases with ContainmentType = NONE - if ($server.versionMajor -gt 10) - { - if ($db.ContainmentType -ne [Microsoft.SqlServer.Management.Smo.ContainmentType]::None) - { - Write-Warning "Database '$db' is a contained database. Contained databases can't have orphaned users. Skipping validation." - Continue - } - } - - Write-Output "Validating users on database '$db'" - - if ($Users.Count -eq 0) - { - #the third validation will remove from list sql users without login. The rule here is Sid with length higher than 16 - $Users = $db.Users | Where-Object {$_.Login -eq "" -and ($_.ID -gt 4) -and ($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false} - } - else - { - if ($pipedatabase.Length -gt 0) - { - $Source = $pipedatabase[3].parent.name - $Users = $pipedatabase.name - } - else - { - #the fourth validation will remove from list sql users without login. The rule here is Sid with length higher than 16 - $Users = $db.Users | Where-Object {$_.Login -eq "" -and ($_.ID -gt 4) -and ($Users -contains $_.Name) -and (($_.Sid.Length -gt 16 -and $_.LoginType -eq [Microsoft.SqlServer.Management.Smo.LoginType]::SqlLogin) -eq $false)} - } - } - - if ($Users.Count -gt 0) - { - Write-Verbose "Orphan users found" - $UsersToRemove = @() - foreach ($User in $Users) - { - $ExistLogin = $server.logins | Where-Object {$_.Isdisabled -eq $False -and - $_.IsSystemObject -eq $False -and - $_.IsLocked -eq $False -and - $_.Name -eq $User.Name } - - if ($ExistLogin) - { - if ($server.versionMajor -gt 8) - { - $query = "ALTER USER " + $User + " WITH LOGIN = " + $User - } - else - { - $query = "exec sp_change_users_login 'update_one', '$User'" - } - - if ($Pscmdlet.ShouldProcess($db.Name, "Mapping user '$($User.Name)'")) - { - $server.Databases[$db.Name].ExecuteNonQuery($query) | Out-Null - Write-Output "`r`nUser '$($User.Name)' mapped with their login" - } - } - else - { - if ($RemoveNotExisting -eq $true) - { - #add user to collection - $UsersToRemove += $User - } - else - { - Write-Warning "Orphan user $($User.Name) does not have matching login." - } - } - } - - #With the colelction complete invoke remove. - if ($RemoveNotExisting -eq $true) - { - if ($Pscmdlet.ShouldProcess($db.Name, "Remove-SqlOrphanUser")) - { - Write-Verbose "Calling 'Remove-SqlOrphanUser'" - Remove-SqlOrphanUser -SqlServer $SqlServer -SqlCredential $SqlCredential -Databases $db.Name -Users $UsersToRemove - } - } - } - else - { - Write-Output "No orphan users found on database '$db'" - } - #reset collection - $Users = $null - } - catch - { - throw $_ - } - } - } - else - { - Write-Output "There are no databases to analyse." - } - } - - END - { - $server.ConnectionContext.Disconnect() - - $totaltime = ($start.Elapsed) - Write-Output "Total Elapsed time: $totaltime" - } -} diff --git a/functions/Reset-DbaAdmin.ps1 b/functions/Reset-DbaAdmin.ps1 new file mode 100644 index 000000000000..a6c54eda0312 --- /dev/null +++ b/functions/Reset-DbaAdmin.ps1 @@ -0,0 +1,445 @@ +function Reset-DbaAdmin { + <# + .SYNOPSIS + This function allows administrators to regain access to SQL Servers in the event that passwords or access was lost. + + Supports SQL Server 2005 and above. Windows administrator access is required. + + .DESCRIPTION + This function allows administrators to regain access to local or remote SQL Servers by either resetting the sa password, adding the sysadmin role to existing login, or adding a new login (SQL or Windows) and granting it sysadmin privileges. + + This is accomplished by stopping the SQL services or SQL Clustered Resource Group, then restarting SQL via the command-line using the /mReset-DbaAdmin parameter which starts the server in Single-User mode and only allows this script to connect. + + Once the service is restarted, the following tasks are performed: + - Login is added if it doesn't exist + - If login is a Windows User, an attempt is made to ensure it exists + - If login is a SQL Login, password policy will be set to OFF when creating the login, and SQL Server authentication will be set to Mixed Mode. + - Login will be enabled and unlocked + - Login will be added to sysadmin role + + If failures occur at any point, a best attempt is made to restart the SQL Server. + + In order to make this script as portable as possible, System.Data.SqlClient and Get-WmiObject are used (as opposed to requiring the Failover Cluster Admin tools or SMO). + + If using this function against a remote SQL Server, ensure WinRM is configured and accessible. If this is not possible, run the script locally. + + Tested on Windows XP, 7, 8.1, Server 2012 and Windows Server Technical Preview 2. + Tested on SQL Server 2005 SP4 through 2016 CTP2. + + .PARAMETER SqlInstance + The SQL Server instance. SQL Server must be 2005 and above, and can be a clustered or stand-alone instance. + + .PARAMETER Login + By default, the Login parameter is "sa" but any other SQL or Windows account can be specified. If a login does not currently exist, it will be added. + + When adding a Windows login to remote servers, ensure the SQL Server can add the login (ie, don't add WORKSTATION\Admin to remoteserver\instance. Domain users and Groups are valid input. + + .PARAMETER SecurePassword + By default, if a SQL Login is detected, you will be prompted for a password. Use this to securely bypass the prompt. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER Force + If this switch is enabled, the Login(s) will be dropped and recreated on Destination. Logins that own Agent jobs cannot be dropped at this time. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlcluster + + Prompts for password, then resets the "sa" account password on sqlcluster. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlserver\sqlexpress -Login ad\administrator + + Prompts user to confirm that they understand the SQL Service will be restarted. + + Adds the domain account "ad\administrator" as a sysadmin to the SQL instance. + If the account already exists, it will be added to the sysadmin role. + + .EXAMPLE + Reset-DbaAdmin -SqlInstance sqlserver\sqlexpress -Login sqladmin -Force + + Skips restart confirmation, prompts for password, then adds a SQL Login "sqladmin" with sysadmin privileges. + If the account already exists, it will be added to the sysadmin role and the password will be reset. + + .NOTES + Tags: WSMan + Author: Chrissy LeMaire (@cl), netnerds.net + Requires: Admin access to server (not SQL Services), + Remoting must be enabled and accessible if $SqlInstance is not local + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + + .LINK + https://dbatools.io/Reset-DbaAdmin +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter] + $SqlInstance, + [string]$Login = "sa", + [SecureString]$SecurePassword, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Reset-SqlAdmin + + #region Utility functions + function ConvertTo-PlainText { + <# + .SYNOPSIS + Internal function. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Security.SecureString] + $Password + ) + + $marshal = [Runtime.InteropServices.Marshal] + $plaintext = $marshal::PtrToStringAuto($marshal::SecureStringToBSTR($Password)) + return $plaintext + } + + function Invoke-ResetSqlCmd { + <# + .SYNOPSIS + Internal function. Executes a SQL statement against specified computer, and uses "Reset-DbaAdmin" as the Application Name. + #> + [OutputType([System.Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter] + $SqlInstance, + [string]$sql + ) + try { + $connstring = "Data Source=$SqlInstance;Integrated Security=True;Connect Timeout=2;Application Name=Reset-DbaAdmin" + $conn = New-Object System.Data.SqlClient.SqlConnection $connstring + $conn.Open() + $cmd = New-Object system.data.sqlclient.sqlcommand($null, $conn) + $cmd.CommandText = $sql + $cmd.ExecuteNonQuery() | Out-Null + $cmd.Dispose() + $conn.Close() + $conn.Dispose() + return $true + } + catch { + return $false + } + } + #endregion Utility functions + } + + process { + if ($Force) { + $ConfirmPreference = "none" + } + + $baseaddress = $SqlInstance.ComputerName + + # Before we continue, we need confirmation. + if ($pscmdlet.ShouldProcess($baseaddress, "Reset-DbaAdmin (SQL Server instance $SqlInstance will restart)")) { + # Get hostname + + if ($baseaddress -eq "." -or $baseaddress -eq $env:COMPUTERNAME -or $baseaddress -eq "localhost") { + $ipaddr = "." + $hostname = $env:COMPUTERNAME + $baseaddress = $env:COMPUTERNAME + } + + # If server is not local, get IP address and NetBios name in case CNAME records were referenced in the SQL hostname + if ($baseaddress -ne $env:COMPUTERNAME) { + # Test for WinRM #Test-WinRM neh + winrm id -r:$baseaddress 2>$null | Out-Null + if ($LastExitCode -ne 0) { + throw "Remote PowerShell access not enabled on on $source or access denied. Quitting." + } + + # Test Connection first using Test-Connection which requires ICMP access then failback to tcp if pings are blocked + Write-Message -Level Verbose -Message "Testing connection to $baseaddress" + $testconnect = Test-Connection -ComputerName $baseaddress -Count 1 -Quiet + + if ($testconnect -eq $false) { + Write-Message -Level Verbose -Message "First attempt using ICMP failed. Trying to connect using sockets. This may take up to 20 seconds." + $tcp = New-Object System.Net.Sockets.TcpClient + try { + $tcp.Connect($hostname, 135) + $tcp.Close() + $tcp.Dispose() + } + catch { + throw "Can't connect to $baseaddress either via ping or tcp (WMI port 135)" + } + } + Write-Message -Level Verbose -Message "Resolving IP address." + try { + $hostentry = [System.Net.Dns]::GetHostEntry($baseaddress) + $ipaddr = ($hostentry.AddressList | Where-Object { $_ -notlike '169.*' } | Select-Object -First 1).IPAddressToString + } + catch { + throw "Could not resolve SqlServer IP or NetBIOS name" + } + + Write-Message -Level Verbose -Message "Resolving NetBIOS name." + try { + $hostname = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=TRUE -ComputerName $ipaddr).PSComputerName + if ($null -eq $hostname) { + $hostname = (nbtstat -A $ipaddr | Where-Object { $_ -match '\<00\> UNIQUE' } | ForEach-Object { $_.SubString(4, 14) }).Trim() + } + } + catch { + throw "Could not access remote WMI object. Check permissions and firewall." + } + } + + # Setup remote session if server is not local + if ($hostname -ne $env:COMPUTERNAME) { + try { + $session = New-PSSession -ComputerName $hostname + } + catch { + throw "Can't access $hostname using PSSession. Check your firewall settings and ensure Remoting is enabled or run the script locally." + } + } + + Write-Message -Level Verbose -Message "Detecting login type." + # Is login a Windows login? If so, does it exist? + if ($login -match "\\") { + Write-Message -Level Verbose -Message "Windows login detected. Checking to ensure account is valid." + $windowslogin = $true + try { + if ($hostname -eq $env:COMPUTERNAME) { + $account = New-Object System.Security.Principal.NTAccount($args) + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + } + else { + Invoke-Command -ErrorAction Stop -Session $session -ArgumentList $login -ScriptBlock { + $account = New-Object System.Security.Principal.NTAccount($args) + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + } + } + } + catch { + Write-Message -Level Warning -Message "Cannot resolve Windows User or Group $login. Trying anyway." + } + } + + # If it's not a Windows login, it's a SQL login, so it needs a password. + if ($windowslogin -ne $true -and (Test-Bound -Not -ParameterName SecurePassword)) { + Write-Message -Level Verbose -Message "SQL login detected" + do { + $Password = Read-Host -AsSecureString "Please enter a new password for $login" + } + while ($Password.Length -eq 0) + } + + If ((Test-Bound -ParameterName SecurePassword)) { + $Password = $SecurePassword + } + + # Get instance and service display name, then get services + $instance = $null + $instance = $SqlInstance.InstanceName + if (-not $instance) { + $instance = "MSSQLSERVER" + } + $displayName = "SQL Server ($instance)" + + try { + if ($hostname -eq $env:COMPUTERNAME) { + $instanceservices = Get-Service -ErrorAction Stop | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } + $sqlservice = Get-Service -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instance)" + } + else { + $instanceservices = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } + $sqlservice = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instance)" + } + } + catch { + Stop-Function -Message "Cannot connect to WMI on $hostname or SQL Service does not exist. Check permissions, firewall and SQL Server running status." -ErrorRecord $_ -Target $SqlInstance + return + } + + if (-not $instanceservices) { + Stop-Function -Message "Couldn't find SQL Server instance. Check the spelling, ensure the service is running and try again." -Target $SqlInstance + return + } + + Write-Message -Level Verbose -Message "Attempting to stop SQL Services." + + # Check to see if service is clustered. Clusters don't support -m (since the cluster service + # itself connects immediately) or -f, so they are handled differently. + try { + $checkcluster = Get-Service -ComputerName $ipaddr -ErrorAction Stop | Where-Object { $_.Name -eq "ClusSvc" -and $_.Status -eq "Running" } + } + catch { + Stop-Function -Message "Can't check services." -Target $SqlInstance -ErrorRecord $_ + return + } + + if ($null -ne $checkcluster) { + $clusterResource = Get-DbaCmObject -ClassName "MSCluster_Resource" -Namespace "root\mscluster" -ComputerName $hostname | + Where-Object { $_.Name.StartsWith("SQL Server") -and $_.OwnerGroup -eq "SQL Server ($instance)" } + } + + # Take SQL Server offline so that it can be started in single-user mode + if ($clusterResource.count -gt 0) { + $isclustered = $true + try { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.TakeOffline(60) } + } + catch { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL Service and quit." -ErrorRecord $_ -Target $SqlInstance + return + } + } + else { + try { + Stop-Service -InputObject $sqlservice -Force -ErrorAction Stop + Write-Message -Level Verbose -Message "Successfully stopped SQL service." + } + catch { + Start-Service -InputObject $instanceservices -ErrorAction Stop + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL service and quit." -ErrorRecord $_ -Target $SqlInstance + return + } + } + + # /mReset-DbaAdmin Starts an instance of SQL Server in single-user mode and only allows this script to connect. + Write-Message -Level Verbose -Message "Starting SQL Service from command line." + try { + if ($hostname -eq $env:COMPUTERNAME) { + $netstart = net start ""$displayname"" /mReset-DbaAdmin 2>&1 + if ("$netstart" -notmatch "success") { + throw + } + } + else { + $netstart = Invoke-Command -ErrorAction Stop -Session $session -ArgumentList $displayname -ScriptBlock { net start ""$args"" /mReset-DbaAdmin } 2>&1 + foreach ($line in $netstart) { + if ($line.length -gt 0) { Write-Message -Level Verbose -Message $line } + } + } + } + catch { + Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue + + if ($isclustered) { + $clusterResource | Where-Object Name -eq "SQL Server" | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object Name -ne "SQL Server" | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + Stop-Function -Message "Couldn't execute net start command. Restarted services and quit." -ErrorRecord $_ + return + } + + Write-Message -Level Verbose -Message "Reconnecting to SQL instance." + try { + $null = Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql "SELECT 1" -ErrorAction Stop + } + catch { + try { + Start-Sleep 3 + $null = Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql "SELECT 1" -ErrorAction Stop + } + catch { + Stop-Service Input-Object $sqlservice -Force -ErrorAction SilentlyContinue + if ($isclustered) { + $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + Stop-Function -Message "Could not stop the SQL Service. Restarted SQL Service and quit." -ErrorRecord $_ + } + } + + # Get login. If it doesn't exist, create it. + Write-Message -Level Verbose -Message "Adding login $login if it doesn't exist." + if ($windowslogin -eq $true) { + $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') + BEGIN CREATE LOGIN [$login] FROM WINDOWS END" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't create login." + } + + } + elseif ($login -ne "sa") { + # Create new sql user + $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') + BEGIN CREATE LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)', CHECK_POLICY = OFF, CHECK_EXPIRATION = OFF END" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't create login." + } + } + + # If $login is a SQL Login, Mixed mode authentication is required. + if ($windowslogin -ne $true) { + Write-Message -Level Verbose -Message "Enabling mixed mode authentication." + Write-Message -Level Verbose -Message "Ensuring account is unlocked." + $sql = "EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'LoginMode', REG_DWORD, 2" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't set to Mixed Mode." + } + + $sql = "ALTER LOGIN [$login] WITH CHECK_POLICY = OFF + ALTER LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)' UNLOCK" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't unlock account." + } + } + + Write-Message -Level Verbose -Message "Ensuring login is enabled." + $sql = "ALTER LOGIN [$login] ENABLE" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't enable login." + } + + if ($login -ne "sa") { + Write-Message -Level Verbose -Message "Ensuring login exists within sysadmin role." + $sql = "EXEC sp_addsrvrolemember '$login', 'sysadmin'" + if ($(Invoke-ResetSqlCmd -SqlInstance $sqlinstance -Sql $sql) -eq $false) { + Write-Message -Level Warning -Message "Couldn't add to sysadmin role." + } + } + + Write-Message -Level Verbose -Message "Finished with login tasks." + Write-Message -Level Verbose -Message "Restarting SQL Server." + Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue + if ($isclustered -eq $true) { + $clusterResource | Where-Object Name -eq "SQL Server" | ForEach-Object { $_.BringOnline(60) } + $clusterResource | Where-Object Name -ne "SQL Server" | ForEach-Object { $_.BringOnline(60) } + } + else { + Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue + } + } + } + end { + Write-Message -Level Verbose -Message "Script complete!" + } +} diff --git a/functions/Reset-SqlAdmin.ps1 b/functions/Reset-SqlAdmin.ps1 deleted file mode 100644 index 01159f6b5d02..000000000000 --- a/functions/Reset-SqlAdmin.ps1 +++ /dev/null @@ -1,466 +0,0 @@ -Function Reset-SqlAdmin -{ -<# -.SYNOPSIS -This function will allow administrators to regain access to SQL Servers in the event that passwords or access was lost. - -Supports SQL Server 2005 and above. Windows administrator access is required. - -.DESCRIPTION -This function allows administrators to regain access to local or remote SQL Servers by either resetting the sa password, adding sysadmin role to existing login, -or adding a new login (SQL or Windows) and granting it sysadmin privileges. - -This is accomplished by stopping the SQL services or SQL Clustered Resource Group, then restarting SQL via the command-line -using the /mReset-SqlAdmin paramter which starts the server in Single-User mode, and only allows this script to connect. - -Once the service is restarted, the following tasks are performed: -- Login is added if it doesn't exist -- If login is a Windows User, an attempt is made to ensure it exists -- If login is a SQL Login, password policy will be set to OFF when creating the login, and SQL Server authentication will be set to Mixed Mode. -- Login will be enabled and unlocked -- Login will be added to sysadmin role - -If failures occur at any point, a best attempt is made to restart the SQL Server. - -In order to make this script as portable as possible, System.Data.SqlClient and Get-WmiObject are used (as opposed to requiring the Failover Cluster Admin tools or SMO). -If using this function against a remote SQL Server, ensure WinRM is configured and accessible. If this is not possible, run the script locally. - -Tested on Windows XP, 7, 8.1, Server 2012 and Windows Server Technical Preview 2. -Tested on SQL Server 2005 SP4 through 2016 CTP2. - - -.PARAMETER SqlServer -The SQL Server instance. SQL Server must be 2005 and above, and can be a clustered or stand-alone instance. - -.PARAMETER Login -By default, the Login parameter is "sa" but any other SQL or Windows account can be specified. If a login does not -currently exist, it will be added. - -When adding a Windows login to remote servers, ensure the SQL Server can add the login (ie, don't add WORKSTATION\Admin to remoteserver\instance. Domain users and -Groups are valid input. - -.PARAMETER Force -By default, a confirmation is presented to ensure the person executing the script knows that a service restart will occur. Force basically performs a -Confirm:$false. This will restart the SQL Service without prompting. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.NOTES -Tags: WSMan -Author: Chrissy LeMaire (@cl), netnerds.net -Requires: Admin access to server (not SQL Services), -Remoting must be enabled and accessible if $sqlserver is not local - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.LINK -https://dbatools.io/Reset-SqlAdmin - -.EXAMPLE -Reset-SqlAdmin -SqlServer sqlcluster - -Prompts for password, then resets the "sa" account password on sqlcluster. - -.EXAMPLE -Reset-SqlAdmin -SqlServer sqlserver\sqlexpress -Login ad\administrator - -Prompts user to confirm that they understand the SQL Service will be restarted. - -Adds the domain account "ad\administrator" as a sysadmin to the SQL instance. -If the account already exists, it will be added to the sysadmin role. - -.EXAMPLE -Reset-SqlAdmin -SqlServer sqlserver\sqlexpress -Login sqladmin -Force - -Skips restart confirmation, prompts for passsword, then adds a SQL Login "sqladmin" with sysadmin privleges. -If the account already exists, it will be added to the sysadmin role and the password will be reset. - -#> - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="High")] - param ( - [Parameter(Mandatory = $true)] - [Alias("ServerInstance","SqlInstance")] - [string]$SqlServer, - [string]$Login = "sa", - [switch]$Force - ) - - BEGIN - { - Function ConvertTo-PlainText - { -<# -.SYNOPSIS -Internal function. - - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Security.SecureString]$Password - ) - - $marshal = [Runtime.InteropServices.Marshal] - $plaintext = $marshal::PtrToStringAuto($marshal::SecureStringToBSTR($Password)) - return $plaintext - } - - Function Invoke-ResetSqlCmd - { - <# - - .SYNOPSIS - Internal function. Executes a SQL statement against specified computer, and uses "Reset-SqlAdmin" as the - Application Name. - - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Alias("ServerInstance","SqlInstance")] - [string]$sqlserver, - [string]$sql - ) - try - { - $connstring = "Data Source=$sqlserver;Integrated Security=True;Connect Timeout=2;Application Name=Reset-SqlAdmin" - $conn = New-Object System.Data.SqlClient.SqlConnection $connstring - $conn.Open() - $cmd = New-Object system.data.sqlclient.sqlcommand($null, $conn) - $cmd.CommandText = $sql - $cmd.ExecuteNonQuery() | Out-Null - $cmd.Dispose() - $conn.Close() - $conn.Dispose() - return $true - } - catch - { - return $false - } - } - } - - PROCESS - { - if ($Force) { $ConfirmPreference="none" } - - $baseaddress = $sqlserver.Split("\")[0] - - # Before we continue, we need confirmation. - if ($pscmdlet.ShouldProcess($baseaddress, "Reset-SqlAdmin (SQL Server instance $sqlserver will restart)")) - { - # Get hostname - - if ($baseaddress -eq "." -or $baseaddress -eq $env:COMPUTERNAME -or $baseaddress -eq "localhost") - { - $ipaddr = "." - $hostname = $env:COMPUTERNAME - $baseaddress = $env:COMPUTERNAME - } - - # If server is not local, get IP address and NetBios name in case CNAME records were referenced in the SQL hostname - if ($baseaddress -ne $env:COMPUTERNAME) - { - # Test for WinRM #Test-WinRM neh - winrm id -r:$baseaddress 2>$null | Out-Null - if ($LastExitCode -ne 0) - { - throw "Remote PowerShell access not enabled on on $source or access denied. Quitting." - } - - # Test Connection first using Test-Connection which requires ICMP access then failback to tcp if pings are blocked - Write-Output "Testing connection to $baseaddress" - $testconnect = Test-Connection -ComputerName $baseaddress -Count 1 -Quiet - - if ($testconnect -eq $false) - { - Write-Output "First attempt using ICMP failed. Trying to connect using sockets. This may take up to 20 seconds." - $tcp = New-Object System.Net.Sockets.TcpClient - try - { - $tcp.Connect($hostname, 135) - $tcp.Close() - $tcp.Dispose() - } - catch - { - throw "Can't connect to $baseaddress either via ping or tcp (WMI port 135)" - } - } - Write-Output "Resolving IP address" - try - { - $hostentry = [System.Net.Dns]::GetHostEntry($baseaddress) - $ipaddr = ($hostentry.AddressList | Where-Object { $_ -notlike '169.*' } | Select -First 1).IPAddressToString - } - catch - { - throw "Could not resolve SqlServer IP or NetBIOS name" - } - - Write-Output "Resolving NetBIOS name" - try - { - $hostname = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=TRUE -ComputerName $ipaddr).PSComputerName - if ($hostname -eq $null) { $hostname = (nbtstat -A $ipaddr | Where-Object { $_ -match '\<00\> UNIQUE' } | ForEach-Object { $_.SubString(4, 14) }).Trim() } - } - catch - { - throw "Could not access remote WMI object. Check permissions and firewall." - } - } - - # Setup remote session if server is not local - if ($hostname -ne $env:COMPUTERNAME) - { - try - { - $session = New-PSSession -ComputerName $hostname - } - catch - { - throw "Can't access $hostname using PSSession. Check your firewall settings and ensure Remoting is enabled or run the script locally." - } - } - - Write-Output "Detecting login type" - # Is login a Windows login? If so, does it exist? - if ($login -match "\\") - { - Write-Output "Windows login detected. Checking to ensure account is valid." - $windowslogin = $true - try - { - if ($hostname -eq $env:COMPUTERNAME) - { - $account = New-Object System.Security.Principal.NTAccount($args) - $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) - } - else - { - Invoke-Command -ErrorAction Stop -Session $session -Args $login -ScriptBlock { - $account = New-Object System.Security.Principal.NTAccount($args) - $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) - } - } - } - catch { Write-Warning "Cannot resolve Windows User or Group $login. Trying anyway." } - } - - # If it's not a Windows login, it's a SQL login, so it needs a password. - if ($windowslogin -ne $true) - { - Write-Output "SQL login detected" - do { $Password = Read-Host -AsSecureString "Please enter a new password for $login" } - while ($Password.Length -eq 0) - } - - # Get instance and service display name, then get services - $instance = ($sqlserver.split("\"))[1] - if ($instance -eq $null) { $instance = "MSSQLSERVER" } - $displayName = "SQL Server ($instance)" - - try - { - if ($hostname -eq $env:COMPUTERNAME) - { - $instanceservices = Get-Service | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } - $sqlservice = Get-Service | Where-Object { $_.DisplayName -eq "SQL Server ($instance)" } - } - else - { - $instanceservices = Get-Service -ComputerName $ipaddr | Where-Object { $_.DisplayName -like "*($instance)*" -and $_.Status -eq "Running" } - $sqlservice = Get-Service -ComputerName $ipaddr | Where-Object { $_.DisplayName -eq "SQL Server ($instance)" } - } - } - catch - { - throw "Cannot connect to WMI on $hostname or SQL Service does not exist. Check permissions, firewall and SQL Server running status." - } - - if ($instanceservices -eq $null) - { - throw "Couldn't find SQL Server instance. Check the spelling, ensure the service is running and try again." - } - - Write-Output "Attempting to stop SQL Services" - - # Check to see if service is clustered. Clusters don't support -m (since the cluster service - # itself connects immediately) or -f, so they are handled differently. - try - { - $checkcluster = Get-Service -ComputerName $ipaddr | Where-Object { $_.Name -eq "ClusSvc" -and $_.Status -eq "Running" } - } - catch - { - throw "Can't check services. Check permissions and firewall." - } - - if ($checkcluster -ne $null) - { - $clusterResource = Get-WmiObject -Authentication PacketPrivacy -Impersonation Impersonate -class "MSCluster_Resource" -namespace "root\mscluster" -computername $hostname | - Where-Object { $_.Name.StartsWith("SQL Server") -and $_.OwnerGroup -eq "SQL Server ($instance)" } - } - - # Take SQL Server offline so that it can be started in single-user mode - if ($clusterResource.count -gt 0) - { - $isclustered = $true - try - { - $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.TakeOffline(60) } - } - catch - { - $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - throw "Could not stop the SQL Service. Restarted SQL Service and quit." - } - } - else - { - try - { - Stop-Service -InputObject $sqlservice -Force - Write-Output "Successfully stopped SQL service" - } - catch - { - Start-Service -InputObject $instanceservices - throw "Could not stop the SQL Service. Restarted SQL service and quit." - } - } - - # /mReset-SqlAdmin Starts an instance of SQL Server in single-user mode and only allows this script to connect. - Write-Output "Starting SQL Service from command line" - try - { - if ($hostname -eq $env:COMPUTERNAME) - { - $netstart = net start ""$displayname"" /mReset-SqlAdmin 2>&1 - if ("$netstart" -notmatch "success") { throw } - } - else - { - $netstart = Invoke-Command -ErrorAction Stop -Session $session -Args $displayname -ScriptBlock { net start ""$args"" /mReset-SqlAdmin } 2>&1 - foreach ($line in $netstart) - { - if ($line.length -gt 0) { Write-Output $line } - } - } - } - catch - { - Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue - - if ($isclustered) - { - $clusterResource | Where-Object Name -eq "SQL Server" | ForEach-Object { $_.BringOnline(60) } - $clusterResource | Where-Object Name -ne "SQL Server" | ForEach-Object { $_.BringOnline(60) } - } - else - { - Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue - } - throw "Couldn't execute net start command. Restarted services and quit." - } - - Write-Output "Reconnecting to SQL instance" - try - { - Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql "SELECT 1" | Out-Null - } - catch - { - try - { - Start-Sleep 3 - Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql "SELECT 1" | Out-Null - } - catch - { - Stop-Service Input-Object $sqlservice -Force -ErrorAction SilentlyContinue - if ($isclustered) - { - $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - } - else { Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue } - throw "Could not stop the SQL Service. Restarted SQL Service and quit." - } - } - - # Get login. If it doesn't exist, create it. - Write-Output "Adding login $login if it doesn't exist" - if ($windowslogin -eq $true) - { - $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') - BEGIN CREATE LOGIN [$login] FROM WINDOWS END" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't create login." } - } - elseif ($login -ne "sa") - { - # Create new sql user - $sql = "IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$login') - BEGIN CREATE LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)', CHECK_POLICY = OFF, CHECK_EXPIRATION = OFF END" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't create login." } - } - - # If $login is a SQL Login, Mixed mode authentication is required. - if ($windowslogin -ne $true) - { - Write-Output "Enabling mixed mode authentication" - Write-Output "Ensuring account is unlocked" - $sql = "EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'LoginMode', REG_DWORD, 2" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't set to Mixed Mode." } - - $sql = "ALTER LOGIN [$login] WITH CHECK_POLICY = OFF - ALTER LOGIN [$login] WITH PASSWORD = '$(ConvertTo-PlainText $Password)' UNLOCK" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't unlock account." } - } - - Write-Output "Ensuring login is enabled" - $sql = "ALTER LOGIN [$login] ENABLE" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't enable login." } - - if ($login -ne "sa") - { - Write-Output "Ensuring login exists within sysadmin role" - $sql = "EXEC sp_addsrvrolemember '$login', 'sysadmin'" - if ($(Invoke-ResetSqlCmd -SqlServer $sqlserver -Sql $sql) -eq $false) { Write-Error "Couldn't add to syadmin role." } - } - - Write-Output "Finished with login tasks" - Write-Output "Restarting SQL Server" - Stop-Service -InputObject $sqlservice -Force -ErrorAction SilentlyContinue - if ($isclustered -eq $true) - { - $clusterResource | Where-Object { $_.Name -eq "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - $clusterResource | Where-Object { $_.Name -ne "SQL Server" } | ForEach-Object { $_.BringOnline(60) } - } - else { Start-Service -InputObject $instanceservices -ErrorAction SilentlyContinue } - } - } - END - { - Write-Output "Script complete!" - } -} diff --git a/functions/Resolve-DbaNetworkName.ps1 b/functions/Resolve-DbaNetworkName.ps1 index b0e55070b504..27d2606f83b0 100644 --- a/functions/Resolve-DbaNetworkName.ps1 +++ b/functions/Resolve-DbaNetworkName.ps1 @@ -1,263 +1,284 @@ -Function Resolve-DbaNetworkName -{ - <# - .SYNOPSIS - Returns information about the network connection of the target computer including NetBIOS name, IP Address, domain name and fully qualified domain name (FQDN). - - .DESCRIPTION - Retrieves the IPAddress, ComputerName from one computer. - The object can be used to take action against its name or IPAddress. - - First ICMP is used to test the connection, and get the connected IPAddress. - - If your local Powershell version is not higher than 2, WMI is tried to get the computername. - If not, CIM is used, first via WinRM, and if not successful, via DCOM. - - .PARAMETER ComputerName - The Server that you're connecting to. - This can be the name of a computer, a SMO object, an IP address or a SQL Instance. - - .PARAMETER Credential - Credential object used to connect to the SQL Server as a different user - - .PARAMETER Turbo - Resolves without accessing the serer itself. Faster but may be less accurate. - - .NOTES - Author: Klaas Vandenberghe ( @PowerDBAKlaas ) - - dbatools PowerShell module (https://dbatools.io) - Copyright (C) 2016 Chrissy LeMaire - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - - .LINK - https://dbatools.io/Resolve-DbaNetworkName - - .EXAMPLE - Resolve-DbaNetworkName -ComputerName ServerA - - Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for ServerA - - .EXAMPLE - Resolve-DbaNetworkName -SqlServer sql2016\sqlexpress - - Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for the SQL instance sql2016\sqlexpress - - .EXAMPLE - Resolve-DbaNetworkName -SqlServer sql2016\sqlexpress, sql2014 - - Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for the SQL instance sql2016\sqlexpress and sql2014 - - Get-SqlRegisteredServerName -SqlServer sql2014 | Resolve-DbaNetworkName - - Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for all SQL Servers returned by Get-SqlRegisteredServerName - #> - [CmdletBinding()] - param ( - [parameter(ValueFromPipeline)] - [Alias('cn', 'host', 'ServerInstance', 'Server', 'SqlServer')] - [object[]]$ComputerName = $env:COMPUTERNAME, - [PSCredential] [System.Management.Automation.CredentialAttribute()]$Credential, - [Alias('FastParrot')] - [switch]$Turbo - ) - BEGIN - { - $functionName = (Get-PSCallstack)[0].Command - } - PROCESS - { - foreach ($Computer in $ComputerName) - { - $conn = $ipaddress = $CIMsession = $null - - if ($Computer.GetType() -eq [Microsoft.SqlServer.Management.Smo.Server]) - { - $Computer = $Computer.NetName - } - - $OGComputer = $Computer - - if ($Computer -eq 'localhost' -or $Computer -eq '.') - { - $Computer = $env:COMPUTERNAME - } - - $Computer = $Computer.Split('\\')[0] - $Computer = ($Computer -Split ('\:'))[0] - $Computer = ($Computer.Split('\,'))[0] - - if ($Turbo) - { - try - { - Write-Verbose "$functionName - Resolving $Computer using .NET.Dns GetHostEntry" - $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString - Write-Verbose "$functionName - Resolving $ipaddress using .NET.Dns GetHostByAddress" - $fqdn = [System.Net.Dns]::GetHostByAddress($ipaddress).HostName - } - catch - { - try - { - Write-Verbose "$functionName - Resolving $Computer and IP using .NET.Dns GetHostEntry" - $resolved = [System.Net.Dns]::GetHostEntry($Computer) - $ipaddress = $resolved.AddressList[0].IPAddressToString - $fqdn = $resolved.HostName - } - catch - { - Write-Warning "$functionName - DNS name not found" - continue - } - } - - if ($fqdn -notmatch "\.") - { - $dnsdomain = $env:USERDNSDOMAIN.ToLower() - $fqdn = "$fqdn.$dnsdomain" - } - - $hostname = $fqdn.Split(".")[0] - - [PSCustomObject]@{ - InputName = $OGComputer - ComputerName = $hostname.ToUpper() - IPAddress = $ipaddress - DNSHostname = $hostname - Domain = $fqdn.Replace("$hostname.", "") - DNSHostEntry = $fqdn - FQDN = $fqdn - } - return - } - - Write-Verbose "$functionName - Connecting to $Computer" - - try - { - $ipaddress = ((Test-Connection -ComputerName $Computer -Count 1 -ErrorAction Stop).Ipv4Address).IPAddressToString - } - catch - { - try - { - $ipaddress = ((Test-Connection -ComputerName "$Computer.$env:USERDNSDOMAIN" -Count 1 -ErrorAction SilentlyContinue).Ipv4Address).IPAddressToString - $Computer = "$Computer.$env:USERDNSDOMAIN" - } - catch - { - $Computer = $OGComputer - $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString - } - } - - if ($ipaddress) - { - Write-Verbose "$functionName - IP Address from $Computer is $ipaddress" - } - else - { - Write-Warning "$functionName - No IP Address returned from $Computer" - Write-Verbose "$functionName - Using .NET.Dns to resolve IP Address" - return (Resolve-DbaNetworkName -ComputerName $Computer -Turbo) - } - - if ($host.Version.Major -gt 2) - { - Write-Verbose "$functionName - Your PowerShell Version is $($host.Version.Major)" - try - { - Write-Verbose "$functionName - Getting computer information from $Computer via CIM (WSMan)" - if ($Credential) - { - $CIMsession = New-CimSession -ComputerName $Computer -ErrorAction SilentlyContinue -Credential $Credential - $conn = Get-CimInstance -Query "Select * FROM Win32_computersystem" -CimSession $CIMsession -ErrorAction SilentlyContinue - } - else - { - $conn = Get-CimInstance -Query "Select * FROM Win32_computersystem" -ComputerName $Computer -ErrorAction SilentlyContinue - } - } - catch - { - Write-Verbose "$functionName - No WSMan connection to $Computer" - } - if (!$conn) - { - try - { - Write-Verbose "$functionName - Getting computer information from $Computer via CIM (DCOM)" - $sessionoption = New-CimSessionOption -Protocol DCOM - if ($Credential) - { - $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue -Credential $Credential - - } - else - { - $CIMsession = New-CimSession -ComputerName $Computer -SessionOption $sessionoption -ErrorAction SilentlyContinue - } - - $conn = Get-CimInstance -Query "Select * FROM Win32_computersystem" -CimSession $CIMsession - } - catch - { - Write-Warning "$functionName - No DCOM connection for CIM to $Computer" - } - } - - if (!$conn) - { - Write-Verbose "$functionName - No CIM from $Computer. Getting HostName via .NET.Dns" - try - { - $fqdn = ([System.Net.Dns]::GetHostEntry($Computer)).HostName - $hostname = $fqdn.Split(".")[0] - - $conn = [PSCustomObject]@{ - Name = $Computer - DNSHostname = $hostname - Domain = $fqdn.Replace("$hostname.", "") - } - } - catch - { - Write-Warning "$functionName - No .NET.Dns information from $Computer" - continue - } - } - } - - - try - { - Write-Verbose "$functionName - Resolving $($conn.DNSHostname) using .NET.Dns GetHostEntry" - $hostentry = ([System.Net.Dns]::GetHostEntry($conn.DNSHostname)).HostName - } - catch - { - Write-Warning "$functionName - .NET.Dns GetHostEntry failed for $($conn.DNSHostname)" - } - - $fqdn = "$($conn.DNSHostname).$($conn.Domain)" - if ($fqdn -eq ".") - { - Write-Verbose "$functionName - No full FQDN found. Setting to null" - $fqdn = $null - } - - [PSCustomObject]@{ - InputName = $OGComputer - ComputerName = $conn.Name - IPAddress = $ipaddress - DNSHostName = $conn.DNSHostname - Domain = $conn.Domain - DNSHostEntry = $hostentry - FQDN = $fqdn - } - } - } +function Resolve-DbaNetworkName { + <# + .SYNOPSIS + Returns information about the network connection of the target computer including NetBIOS name, IP Address, domain name and fully qualified domain name (FQDN). + + .DESCRIPTION + Retrieves the IPAddress, ComputerName from one computer. + The object can be used to take action against its name or IPAddress. + + First ICMP is used to test the connection, and get the connected IPAddress. + + Multiple protocols (e.g. WMI, CIM, etc) are attempted before giving up. + + Important: Remember that FQDN doesn't always match "ComputerName dot Domain" as AD intends. + There are network setup (google "disjoint domain") where AD and DNS do not match. + "Full computer name" (as reported by sysdm.cpl) is the only match between the two, + and it matches the "DNSHostName" property of the computer object stored in AD. + This means that the notation of FQDN that matches "ComputerName dot Domain" is incorrect + in those scenarios. + In other words, the "suffix" of the FQDN CAN be different from the AD Domain. + + This cmdlet has been providing good results since its inception but for lack of useful + names some doubts may arise. + Let this clear the doubts: + - InputName: whatever has been passed in + - ComputerName: hostname only + - IPAddress: IP Address + - DNSHostName: hostname only, coming strictly from DNS (as reported from the calling computer) + - DNSDomain: domain only, coming strictly from DNS (as reported from the calling computer) + - Domain: domain only, coming strictly from AD (i.e. the domain the ComputerName is joined to) + - DNSHostEntry: Fully name as returned by DNS [System.Net.Dns]::GetHostEntry + - FQDN: "legacy" notation of ComputerName "dot" Domain (coming from AD) + - FullComputerName: Full name as configured from within the Computer (i.e. the only secure match between AD and DNS) + + So, if you need to use something, go with FullComputerName, always, as it is the most correct in every scenario. + + .PARAMETER ComputerName + The Server that you're connecting to. + This can be the name of a computer, a SMO object, an IP address or a SQL Instance. + + .PARAMETER Credential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Turbo + Resolves without accessing the server itself. Faster but may be less accurate because it relies on DNS only, + so it may fail spectacularly for disjoin-domain setups. Also, everyone has its own DNS (i.e. results may vary + changing the computer where the function runs) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Network, Resolve + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Editor: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Resolve-DbaNetworkName + + .EXAMPLE + Resolve-DbaNetworkName -ComputerName ServerA + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for ServerA + + .EXAMPLE + Resolve-DbaNetworkName -SqlInstance sql2016\sqlexpress + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for the SQL instance sql2016\sqlexpress + + .EXAMPLE + Resolve-DbaNetworkName -SqlInstance sql2016\sqlexpress, sql2014 + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, DNSDomain, Domain, DNSHostEntry, FQDN, DNSHostEntry for the SQL instance sql2016\sqlexpress and sql2014 + + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sql2014 | Resolve-DbaNetworkName + + Returns a custom object displaying InputName, ComputerName, IPAddress, DNSHostName, Domain, FQDN for all SQL Servers returned by Get-DbaRegisteredServer + #> + [CmdletBinding()] + param ( + [parameter(ValueFromPipeline)] + [Alias('cn', 'host', 'ServerInstance', 'Server', 'SqlInstance')] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential] $Credential, + [Alias('FastParrot')] + [switch]$Turbo, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($Computer in $ComputerName) { + $conn = $ipaddress = $null + + $OGComputer = $Computer + + if ($Computer.IsLocalhost) { + $Computer = $env:COMPUTERNAME + } + else { + $Computer = $Computer.ComputerName + } + + if ($Turbo) { + try { + Write-Message -Level VeryVerbose -Message "Resolving $Computer using .NET.Dns GetHostEntry" + $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + Write-Message -Level VeryVerbose -Message "Resolving $ipaddress using .NET.Dns GetHostByAddress" + $fqdn = [System.Net.Dns]::GetHostByAddress($ipaddress).HostName + } + catch { + try { + Write-Message -Level VeryVerbose -Message "Resolving $Computer and IP using .NET.Dns GetHostEntry" + $resolved = [System.Net.Dns]::GetHostEntry($Computer) + $ipaddress = $resolved.AddressList[0].IPAddressToString + $fqdn = $resolved.HostName + } + catch { + Stop-Function -Message "DNS name not found" -Continue -InnerErrorRecord $_ + } + } + + if ($fqdn -notmatch "\.") { + if ($computer.ComputerName -match "\.") { + $dnsdomain = $computer.ComputerName.Substring($computer.ComputerName.IndexOf(".") + 1) + $fqdn = "$resolved.$dnsdomain" + } + else { + $dnsdomain = "$env:USERDNSDOMAIN".ToLower() + if ($dnsdomain -match "\.") { + $fqdn = "$fqdn.$dnsdomain" + } + } + } + + $hostname = $fqdn.Split(".")[0] + + [PSCustomObject]@{ + InputName = $OGComputer + ComputerName = $hostname.ToUpper() + IPAddress = $ipaddress + DNSHostname = $hostname + DNSDomain = $fqdn.Replace("$hostname.", "") + Domain = $fqdn.Replace("$hostname.", "") + DNSHostEntry = $fqdn + FQDN = $fqdn + FullComputerName = $fqdn + } + + } + else { + + Write-Message -Level Verbose -Message "Connecting to $Computer" + + try { + $ipaddress = ((Test-Connection -ComputerName $Computer -Count 1 -ErrorAction Stop).Ipv4Address).IPAddressToString + } + catch { + try { + if ($env:USERDNSDOMAIN) { + $ipaddress = ((Test-Connection -ComputerName "$Computer.$env:USERDNSDOMAIN" -Count 1 -ErrorAction SilentlyContinue).Ipv4Address).IPAddressToString + $Computer = "$Computer.$env:USERDNSDOMAIN" + } + } + catch { + $Computer = $OGComputer + $ipaddress = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + } + } + + if ($ipaddress) { + Write-Message -Level VeryVerbose -Message "IP Address from $Computer is $ipaddress" + } + else { + Write-Message -Level VeryVerbose -Message "No IP Address returned from $Computer" + Write-Message -Level VeryVerbose -Message "Using .NET.Dns to resolve IP Address" + return (Resolve-DbaNetworkName -ComputerName $Computer -Turbo) + } + + if ($PSVersionTable.PSVersion.Major -gt 2) { + Write-Message -Level System -Message "Your PowerShell Version is $($PSVersionTable.PSVersion.Major)" + try { + try { + # if an alias (CNAME) is passed we should try to connect to the A name via CIM or WinRM + $ComputerNameIP = ([System.Net.Dns]::GetHostEntry($Computer)).AddressList[0].IPAddressToString + $RemoteComputer = [System.Net.Dns]::GetHostByAddress($ComputerNameIP).HostName + } + catch { + $RemoteComputer = $Computer + } + Write-Message -Level VeryVerbose -Message "Getting computer information from $RemoteComputer" + $ScBlock = { + $IPGProps = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() + return [pscustomobject]@{ + 'DNSDomain' = $IPGProps.DomainName + } + } + if (Test-Bound "Credential") { + $conn = Get-DbaCmObject -ClassName win32_ComputerSystem -Computer $RemoteComputer -Credential $Credential -EnableException + $DNSSuffix = Invoke-Command2 -Computer $RemoteComputer -ScriptBlock $ScBlock -Credential $Credential -ErrorAction Stop + } + else { + $conn = Get-DbaCmObject -ClassName win32_ComputerSystem -Computer $RemoteComputer -EnableException + $DNSSuffix = Invoke-Command2 -Computer $RemoteComputer -ScriptBlock $ScBlock -ErrorAction Stop + } + } + catch { + Write-Message -Level Verbose -Message "Unable to get computer information from $Computer" + } + + if (!$conn) { + Write-Message -Level Verbose -Message "No WMI/CIM from $Computer. Getting HostName via .NET.Dns" + try { + $fqdn = ([System.Net.Dns]::GetHostEntry($Computer)).HostName + $hostname = $fqdn.Split(".")[0] + $suffix = $fqdn.Replace("$hostname.", "") + if ($hostname -eq $fqdn) { + $suffix = "" + } + $conn = [PSCustomObject]@{ + Name = $Computer + DNSHostname = $hostname + Domain = $suffix + } + $DNSSuffix = [PSCustomObject]@{ + DNSDomain = $suffix + } + } + catch { + Stop-Function -Message "No .NET.Dns information from $Computer" -InnerErrorRecord $_ -Continue + } + } + } + if ($DNSSuffix.DNSDomain.Length -eq 0) { + $FullComputerName = $conn.DNSHostname + } + else { + $FullComputerName = $conn.DNSHostname + "." + $DNSSuffix.DNSDomain + } + try { + Write-Message -Level VeryVerbose -Message "Resolving $FullComputerName using .NET.Dns GetHostEntry" + $hostentry = ([System.Net.Dns]::GetHostEntry($FullComputerName)).HostName + } + catch { + Stop-Function -Message ".NET.Dns GetHostEntry failed for $FullComputerName" -InnerErrorRecord $_ + } + + $fqdn = "$($conn.DNSHostname).$($conn.Domain)" + if ($fqdn -eq ".") { + Write-Message -Level VeryVerbose -Message "No full FQDN found. Setting to null" + $fqdn = $null + } + if ($FullComputerName -eq ".") { + Write-Message -Level VeryVerbose -Message "No DNS FQDN found. Setting to null" + $FullComputerName = $null + } + + if ($FullComputerName -ne "." -and $FullComputerName -notmatch "\." -and $conn.Domain -match "\.") { + $d = $conn.Domain + $FullComputerName = "$FullComputerName.$d" + } + + [PSCustomObject]@{ + InputName = $OGComputer + ComputerName = $conn.Name + IPAddress = $ipaddress + DNSHostName = $conn.DNSHostname + DNSDomain = $DNSSuffix.DNSDomain + Domain = $conn.Domain + DNSHostEntry = $hostentry + FQDN = $fqdn.TrimEnd(".") + FullComputerName = $FullComputerName + } + } + } + } } diff --git a/functions/Restart-DbaSqlService.ps1 b/functions/Restart-DbaSqlService.ps1 new file mode 100644 index 000000000000..ccf10dddebf3 --- /dev/null +++ b/functions/Restart-DbaSqlService.ps1 @@ -0,0 +1,143 @@ +function Restart-DbaSqlService { + <# + .SYNOPSIS + Restarts SQL Server services on a computer. + + .DESCRIPTION + Restarts the SQL Server related services on one or more computers. Will follow SQL Server service dependencies. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER InstanceName + Only affects services that belong to the specific instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER Type + Use -Type to collect only services of the desired SqlServiceType. + Can be one of the following: "Agent","Browser","Engine","FullText","SSAS","SSIS","SSRS" + + .PARAMETER Timeout + How long to wait for the start/stop request completion before moving on. Specify 0 to wait indefinitely. + + .PARAMETER InputObject + A collection of services from Get-DbaSqlService + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER Force + Will stop dependent SQL Server agents when stopping Engine services. + + .NOTES + Tags: Service, SqlServer, Instance, Connect + Author: Kirill Kravtsov( @nvarscar ) + + Requires Local Admin rights on destination computer(s). + + dbatools PowerShell module (https://dbatools.io) + Copyright (C) 2017 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restart-DbaSqlService + + .EXAMPLE + Restart-DbaSqlService -ComputerName sqlserver2014a + + Restarts the SQL Server related services on computer sqlserver2014a. + + .EXAMPLE + 'sql1','sql2','sql3'| Get-DbaSqlService | Restart-DbaSqlService + + Gets the SQL Server related services on computers sql1, sql2 and sql3 and restarts them. + + .EXAMPLE + Restart-DbaSqlService -ComputerName sql1,sql2 -Instance MSSQLSERVER + + Restarts the SQL Server services related to the default instance MSSQLSERVER on computers sql1 and sql2. + + .EXAMPLE + Restart-DbaSqlService -ComputerName $MyServers -Type SSRS + + Restarts the SQL Server related services of type "SSRS" (Reporting Services) on computers in the variable MyServers. + + .EXAMPLE + Restart-DbaSqlService -ComputerName sql1 -Type Engine -Force + + Restarts SQL Server database engine services on sql1 forcing dependent SQL Server Agent services to restart as well. + #> + [CmdletBinding(DefaultParameterSetName = "Server", SupportsShouldProcess = $true)] + Param ( + [Parameter(ParameterSetName = "Server", Position = 1)] + [Alias("cn", "host", "Server")] + [DbaInstanceParameter[]]$ComputerName = $env:COMPUTERNAME, + [Alias("Instance")] + [string[]]$InstanceName, + [ValidateSet("Agent", "Browser", "Engine", "FullText", "SSAS", "SSIS", "SSRS")] + [string[]]$Type, + [parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = "Service")] + [Alias("ServiceCollection")] + [object[]]$InputObject, + [int]$Timeout = 30, + [PSCredential]$Credential, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $processArray = @() + if ($PsCmdlet.ParameterSetName -eq "Server") { + $serviceParams = @{ ComputerName = $ComputerName } + if ($InstanceName) { $serviceParams.InstanceName = $InstanceName } + if ($Type) { $serviceParams.Type = $Type } + if ($Credential) { $serviceParams.Credential = $Credential } + if ($EnableException) { $serviceParams.Silent = $EnableException } + $InputObject = Get-DbaSqlService @serviceParams + } + } + process { + #Get all the objects from the pipeline before proceeding + $processArray += $InputObject + } + end { + $processArray = [array]($processArray | Where-Object { (!$InstanceName -or $_.InstanceName -in $InstanceName) -and (!$Type -or $_.ServiceType -in $Type) }) + foreach ($service in $processArray) { + if ($Force -and $service.ServiceType -eq 'Engine' -and !($processArray | Where-Object { $_.ServiceType -eq 'Agent' -and $_.InstanceName -eq $service.InstanceName -and $_.ComputerName -eq $service.ComputerName })) { + Write-Message -Level Verbose -Message "Adding Agent service to the list for service $($service.ServiceName) on $($service.ComputerName), since -Force has been specified" + #Construct parameters to call Get-DbaSqlService + $serviceParams = @{ + ComputerName = $service.ComputerName + InstanceName = $service.InstanceName + Type = 'Agent' + } + if ($Credential) { $serviceParams.Credential = $Credential } + if ($EnableException) { $serviceParams.Silent = $EnableException } + $processArray += @(Get-DbaSqlService @serviceParams) + } + } + if ($processArray) { + $services = Update-ServiceStatus -InputObject $processArray -Action 'stop' -Timeout $Timeout -EnableException $EnableException + foreach ($service in ($services | Where-Object { $_.Status -eq 'Failed'})) { + $service + } + $services = $services | Where-Object { $_.Status -eq 'Successful'} + if ($services) { + Update-ServiceStatus -InputObject $services -Action 'restart' -Timeout $Timeout -EnableException $EnableException + } + } + else { Stop-Function -EnableException $EnableException -Message "No SQL Server services found with current parameters." } + } +} diff --git a/functions/Restore-DbaBackupFromDirectory.ps1 b/functions/Restore-DbaBackupFromDirectory.ps1 new file mode 100644 index 000000000000..0bbda58f987b --- /dev/null +++ b/functions/Restore-DbaBackupFromDirectory.ps1 @@ -0,0 +1,68 @@ +function Restore-DbaBackupFromDirectory { + <# + .SYNOPSIS + Restores SQL Server databases from the backup directory structure created by Ola Hallengren's database maintenance scripts. Different structures coming soon. + + .DESCRIPTION + Many SQL Server database administrators use Ola Hallengren's SQL Server Maintenance Solution which can be found at http://ola.hallengren.com + + Hallengren uses a predictable backup structure which made it relatively easy to create a script that can restore an entire SQL Server database instance, down to the master database (next version), to a new server. This script is intended to be used in the event that the originating SQL Server becomes unavailable, thus rendering my other SQL restore script (http://goo.gl/QmfQ6s) ineffective. + + .PARAMETER SqlInstance + The SQL Server instance to which you will be restoring the database. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Path + Specifies the full path to the directory that contains the database backups. The SQL Server service must have read access to this path. + + .PARAMETER ReuseSourceFolderStructure + If this switch is enabled, the folder structure used on the instance where the backup was made will be recreated. By default, the database files will be restored to the default data and log directories for the instance you're restoring onto. + + .PARAMETER NoRecovery + If this switch is enabled, the database is left in the No Recovery state to enable further backups to be added. + + .PARAMETER Force + If this switch is enabled, any existing database matching the name of a database being restored will be overwritten. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Requires: sysadmin access on destination SQL Server. + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restore-SqlBackupFromDirectory + + .EXAMPLE + Restore-SqlBackupFromDirectory -SqlInstance sqlcluster -Path \\fileserver\share\sqlbackups\SQLSERVER2014A + + All user databases contained within \\fileserver\share\sqlbackups\SQLSERVERA will be restored to sqlcluster, down the most recent full/differential/logs. + + #> + #Requires -Version 3.0 + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [parameter(Mandatory = $true)] + [string]$Path, + [switch]$NoRecovery, + [Alias("ReuseFolderStructure")] + [switch]$ReuseSourceFolderStructure, + [PSCredential]$SqlCredential, + [switch]$Force + ) + + Write-Warning "This command is no longer supported. Please use Get-ChildItem | Restore-DbaDatabase instead" +} diff --git a/functions/Restore-DbaDatabase.ps1 b/functions/Restore-DbaDatabase.ps1 index 022665952d0e..911eeeebe948 100644 --- a/functions/Restore-DbaDatabase.ps1 +++ b/functions/Restore-DbaDatabase.ps1 @@ -1,530 +1,692 @@ -function Restore-DbaDatabase -{ -<# -.SYNOPSIS -Restores a SQL Server Database from a set of backupfiles +function Restore-DbaDatabase { + <# + .SYNOPSIS + Restores a SQL Server Database from a set of backupfiles -.DESCRIPTION -Upon bein passed a list of potential backups files this command will scan the files, select those that contain SQL Server -backup sets. It will then filter those files down to a set that can perform the requested restore, checking that we have a -full restore chain to the point in time requested by the caller. + .DESCRIPTION + Upon being passed a list of potential backups files this command will scan the files, select those that contain SQL Server + backup sets. It will then filter those files down to a set that can perform the requested restore, checking that we have a + full restore chain to the point in time requested by the caller. -The function defaults to working on a remote instance. This means that all paths passed in must be relative to the remote instance. -XpDirTree will be used to perform the file scans + The function defaults to working on a remote instance. This means that all paths passed in must be relative to the remote instance. + XpDirTree will be used to perform the file scans -Various means can be used to pass in a list of files to be considered. The default is to non recursively scan the folder -passed in. + Various means can be used to pass in a list of files to be considered. The default is to non recursively scan the folder + passed in. -.PARAMETER SqlServer -The SQL Server instance to restore to. + .PARAMETER SqlInstance + The SQL Server instance to restore to. -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. + .PARAMETER SqlCredential + Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. -.PARAMETER Path -Path to SQL Server backup files. + .PARAMETER Path + Path to SQL Server backup files. -Paths passed in as strings will be scanned using the desired method, default is a non recursive folder scan -Accepts multiple paths seperated by ',' + Paths passed in as strings will be scanned using the desired method, default is a non recursive folder scan + Accepts multiple paths separated by ',' -Or it can consist of FileInfo objects, such as the output of Get-ChildItem or Get-Item. This allows you to work with -your own filestructures as needed + Or it can consist of FileInfo objects, such as the output of Get-ChildItem or Get-Item. This allows you to work with + your own filestructures as needed -.PARAMETER DestinationDataDirectory -Path to restore the SQL Server backups to on the target instance. -If only this parameter is specified, then all database files (data and log) will be restored to this location + .PARAMETER DatabaseName + Name to restore the database under. + Only works with a single database restore. If multiple database are found in the provided paths then we will exit -.PARAMETER DestinationLogDirectory -Path to restore the database log files to. -This parameter can only be specified alongside DestinationDataDirectory. + .PARAMETER DestinationDataDirectory + Path to restore the SQL Server backups to on the target instance. + If only this parameter is specified, then all database files (data and log) will be restored to this location -.PARAMETER DestinationFilePrefix -This value will be prefixed to ALL restored files (log and data). This is just a simple string prefix. If you -want to perform more complex rename operations then please use the FileMapping parameter + .PARAMETER DestinationLogDirectory + Path to restore the database log files to. + This parameter can only be specified alongside DestinationDataDirectory. -This will apply to all file move options, except for FileMapping + .PARAMETER DestinationFileStreamDirectory + Path to restore FileStream data to + This parameter can only be specified alongside DestinationDataDirectory -.PARAMETER UseDestinationDefaultDirectories -Switch that tells the restore to use the default Data and Log locations on the target server. If the don't exist, -the function will try to create them + .PARAMETER RestoreTime + Specify a DateTime object to which you want the database restored to. Default is to the latest point available in the specified backups -.PARAMETER RestoreTime -Specify a DateTime object to which you want the database restored to. Default is to the latest point available -in the specified backups + .PARAMETER NoRecovery + Indicates if the databases should be recovered after last restore. Default is to recover -.PARAMETER MaintenanceSolutionBackup -Switch to indicate the backup files are in a folder structure as created by Ola Hallengreen's maintenance scripts. -This swith enables a faster check for suitable backups. Other options require all files to be read first to ensure -we have an anchoring full backup. Because we can rely on specific locations for backups performed with OlaHallengren's -backup solution, we can rely on file locations. + .PARAMETER WithReplace + Switch indicated is the restore is allowed to replace an existing database. -.PARAMETER DatabaseName -Name to restore the database under. -Only works with a single database restore. If multiple database are found in the provided paths then we will exit + .PARAMETER XpDirTree + Switch that indicated file scanning should be performed by the SQL Server instance using xp_dirtree + This will scan recursively from the passed in path + You must have sysadmin role membership on the instance for this to work. -.PARAMETER RestoredDatababaseNamePrefix -A string which will be prefixed to the start of the restore Database's Name -Useful if restoring a copy to the same sql sevrer for testing. + .PARAMETER OutputScriptOnly + Switch indicates that ONLY T-SQL scripts should be generated, no restore takes place -.PARAMETER NoRecovery -Indicates if the databases should be recovered after last restore. Default is to recover + .PARAMETER VerifyOnly + Switch indicate that restore should be verified -.PARAMETER WithReplace -Switch indicated is the restore is allowed to replace an existing database. + .PARAMETER MaintenanceSolutionBackup + Switch to indicate the backup files are in a folder structure as created by Ola Hallengreen's maintenance scripts. + This swith enables a faster check for suitable backups. Other options require all files to be read first to ensure we have an anchoring full backup. Because we can rely on specific locations for backups performed with OlaHallengren's backup solution, we can rely on file locations. -.PARAMETER OutputScriptOnly -Switch indicates that ONLY T-SQL scripts should be generated, no restore takes place + .PARAMETER FileMapping + A hashtable that can be used to move specific files to a location. + $FileMapping = @{'DataFile1'='c:\restoredfiles\Datafile1.mdf';'DataFile3'='d:\DataFile3.mdf'} + And files not specified in the mapping will be restored to their original location + This Parameter is exclusive with DestinationDataDirectory -.PARAMETER VerifyOnly -Switch indicate that restore should be verified + .PARAMETER IgnoreLogBackup + This switch tells the function to ignore transaction log backups. The process will restore to the latest full or differential backup point only -.PARAMETER XpDirTree -Switch that indicated file scanning should be performed by the SQL Server instance using xp_dirtree -This will scan recursively from the passed in path -You must have sysadmin role membership on the instance for this to work. + .PARAMETER useDestinationDefaultDirectories + Switch that tells the restore to use the default Data and Log locations on the target server. If they don't exist, the function will try to create them -.PARAMETER FileMapping -A hashtable that can be used to move specific files to a location. -$FileMapping = @{'DataFile1'='c:\restoredfiles\Datafile1.mdf';'DataFile3'='d:\DataFile3.mdf'} -And files not specified in the mapping will be restore to their original location -This Parameter is exclusive with DestinationDataDirectory + .PARAMETER ReuseSourceFolderStructure + By default, databases will be migrated to the destination Sql Server's default data and log directories. You can override this by specifying -ReuseSourceFolderStructure. + The same structure on the SOURCE will be kept exactly, so consider this if you're migrating between different versions and use part of Microsoft's default Sql structure (MSSql12.INSTANCE, etc) -.PARAMETER IgnoreLogBackup -This switch tells the function to ignore transaction log backups. The process will restore to the latest full or differential backup point only + *Note, to reuse destination folder structure, specify -WithReplace -.PARAMETER ReuseSourceFolderStructure -By default, databases will be migrated to the destination Sql Server's default data and log directories. You can override this by specifying -ReuseSourceFolderStructure. -The same structure on the SOURCE will be kept exactly, so consider this if you're migrating between different versions and use part of Microsoft's default Sql structure (MSSql12.INSTANCE, etc) + .PARAMETER DestinationFilePrefix + This value will be prefixed to ALL restored files (log and data). This is just a simple string prefix. If you want to perform more complex rename operations then please use the FileMapping parameter -*Note, to reuse destination folder structure, specify -WithReplace - -.PARAMETER Confirm -Prompts to confirm certain actions - -.PARAMETER WhatIf -Shows what would happen if the command would execute, but does not actually perform the command + This will apply to all file move options, except for FileMapping -.NOTES -Tags: DisasterRecovery, Backup, Restore -Original Author: Stuart Moore (@napalmgram), stuart-moore.com + .PARAMETER DestinationFileSuffix + This value will be suffixed to ALL restored files (log and data). This is just a simple string suffix. If you want to perform more complex rename operations then please use the FileMapping parameter -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire + This will apply to all file move options, except for FileMapping -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + .PARAMETER RestoredDatabaseNamePrefix + A string which will be prefixed to the start of the restore Database's Name + Useful if restoring a copy to the same sql server for testing. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + .PARAMETER TrustDbBackupHistory + This switch can be used when piping the output of Get-DbaBackupHistory or Backup-DbaDatabase into this command. + It allows the user to say that they trust that the output from those commands is correct, and skips the file header read portion of the process. This means a faster process, but at the risk of not knowing till halfway through the restore that something is wrong with a file. -You should have received a copy of the GNU General Public License along with this program. If not, see . + .PARAMETER MaxTransferSize + Parameter to set the unit of transfer. Values must be a multiple by 64kb -.EXAMPLE -Restore-DbaDatabase -SqlServer server1\instance1 -Path \\server2\backups + .PARAMETER Blocksize + Specifies the block size to use. Must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb or 64kb + Can be specified in bytes + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail -Scans all the backup files in \\server2\backups, filters them and restores the database to server1\instance1 + .PARAMETER BufferCount + Number of I/O buffers to use to perform the operation. + Refer to https://msdn.microsoft.com/en-us/library/ms178615.aspx for more detail -.EXAMPLE -Restore-DbaDatabase -SqlServer server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores + .PARAMETER XpNoRecurse + If specified, prevents the XpDirTree process from recursing (its default behaviour) -Scans all the backup files in \\server2\backups$ stored in an Ola Hallengren style folder structure, - filters them and restores the database to the c:\restores folder on server1\instance1 + .PARAMETER DirectoryRecurse + If specified the specified directory will be recursed into -.EXAMPLE -Get-ChildItem c:\SQLbackups1\, \\server\sqlbackups2 | Restore-DbaDatabase -SqlServer server1\instance1 + .PARAMETER Continue + If specified we will to attempt to recover more transaction log backups onto database(s) in Recovering or Standby states -Takes the provided files from multiple directories and restores them on server1\instance1 + .PARAMETER StandbyDirectory + If a directory is specified the database(s) will be restored into a standby state, with the standby file placed into this directory (which must exist, and be writable by the target Sql Server instance) -.EXAMPLE -$RestoreTime = Get-Date('11:19 23/12/2016') -Restore-DbaDatabase -SqlServer server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores -RestoreTime $RestoreTime + .PARAMETER AzureCredential + The name of the SQL Server credential to be used if restoring from an Azure hosted backup -Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, - filters them and restores the database to the c:\restores folder on server1\instance1 up to 11:19 23/12/2016 + .PARAMETER ReplaceDbNameInFile + If switch set and occurence of the original database's name in a data or log file will be replace with the name specified in the Databasename parameter -.EXAMPLE -Restore-DbaDatabase -SqlServer server1\instance1 -Path \\server2\backups -DestinationDataDirectory c:\restores -OutputScriptOnly | Select-Object -ExpandPropert Tsql | Out-File -Filepath c:\scripts\restore.sql + .PARAMETER Recover + If set will perform recovery on the indicated database -Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, - filters them and generate the T-SQL Scripts to restore the database to the latest point in time, - and then stores the output in a file for later retrieval + .PARAMETER AllowContinue + By default, Restore-DbaDatabase will stop restoring any databases if it comes across an error. + Use this switch to enable it to restore all databases without issues. -.EXAMPLE -Restore-DbaDatabase -SqlServer server1\instance1 -Path c:\backups -DestinationDataDirectory c:\DataFiles -DestinationLogDirectory c:\LogFile + .PARAMETER GetBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Get-DbaBackupInformation -Scans all the files in c:\backups and then restores them onto the SQL Server Instance server1\instance1, placing data files -c:\DataFiles and all the log files into c:\LogFiles - -.EXAMPLE -$File = Get-ChildItem c:\backups, \\server1\backups -recurse -$File | Restore-DbaDatabase -SqlServer Server1\Instance -UseDestinationDefaultDirectories + .PARAMETER SelectBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Select-DbaBackupInformation -This will take all of the files found under the folders c:\backups and \\server1\backups, and pipeline them into -Restore-DbaDatabase. Restore-DbaDatabase will then scan all of the files, and restore all of the databases included -to the latest point in time covered by their backups. All data and log files will be moved to the default SQL Sever -folder for those file types as defined on the target instance. + .PARAMETER FormatBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Format-DbaBackupInformation -#> - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [object[]]$Path, - [parameter(Mandatory = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential, - [string]$DatabaseName, - [String]$DestinationDataDirectory, - [String]$DestinationLogDirectory, - [DateTime]$RestoreTime = (Get-Date).addyears(1), - [switch]$NoRecovery, - [switch]$WithReplace, - [Switch]$XpDirTree, - [switch]$OutputScriptOnly, - [switch]$VerifyOnly, - [switch]$MaintenanceSolutionBackup, - [hashtable]$FileMapping, - [switch]$IgnoreLogBackup, - [switch]$UseDestinationDefaultDirectories, - [switch]$ReuseSourceFolderStructure, - [string]$DestinationFilePrefix = '', - [string]$RestoredDatababaseNamePrefix - ) - BEGIN - { - #Don't like nulls - $islocal = $false - $base = $SqlServer.Split("\")[0] - - if ($base -eq "." -or $base -eq "localhost" -or $base -eq $env:computername -or $base -eq "127.0.0.1") - { - $islocal = $true - } - - $FunctionName = $FunctionName = (Get-PSCallstack)[0].Command - $BackupFiles = @() - $UseDestinationDefaultDirectories = $true - #Check compatible relocation options used: - - $ParamCount = 0 - if ($null -ne $FileMapping) - { - $ParamCount += 1 - } - if ($ReuseSourceFolderStructure) - { - $ParamCount += 1 - } - if ('' -ne $DestinationDataDirectory) - { - $ParamCount += 1 - } - if ($ParamCount -gt 1) - { - Write-Warning "$FunctionName - $Paramcount You've specified incompatible Location parameters. Please only specify one of FileMapping,ReuseSourceFolderStructure or DestinationDataDirectory" - break - } - - if ($DestinationLogDirectory -ne '' -and $ReuseSourceFolderStructure) - { - Write-Warning "$FunctionName - DestinationLogDirectory and UseDestinationDefaultDirectories are mutually exclusive" - break - } - if ($DestinationLogDirectory -ne '' -and $DestinationDataDirectory -eq '') - { - Write-Warning "$FunctionName - DestinationLogDirectory can only be specified with DestinationDataDirectory" - break - } - if (($null -ne $FileMapping) -or $ReuseSourceFolderStructure -or ($DestinationDataDirectory -ne '')) - { - $UseDestinationDefaultDirectories = $false - } - - } - PROCESS - { - foreach ($f in $path) - { - if ($f.FullName) - { - $f = $f.FullName - } - - if ($f.Gettype -is [string]) - { - if ($f.StartsWith("\\") -eq $false -and $islocal -ne $true) - { - Write-Verbose "$FunctionName - Working remotely, and non UNC path used. Dropping to XpDirTree, all paths evaluated at $SqlServer" - # Many internal functions parse using Get-ChildItem. - # We need to use Test-SqlPath and other commands instead - # Prevent people from trying - - #Write-Warning "Currently, you can only use UNC paths when running this command remotely. We expect to support non-UNC paths for remote servers shortly." - #continue - - #$newpath = Join-AdminUnc $SqlServer "$path" - #Write-Warning "Run this command on the server itself or try $newpath." - if ($XpDirTree -ne $true) - { - Write-Verbose "$FunctionName - Only XpDirTree is safe on remote server" - $XpDirTree = $true - $MaintenanceSolutionBackup = $false - } - } - } - - Write-Verbose "$FunctionName - type = $($f.gettype())" - if ($f -is [string]) - { - Write-Verbose "$FunctionName : Paths passed in" - foreach ($p in $f) - { - if ($XpDirTree) - { - if ($p -match '\.\w{3}\Z' ) - { - if (Test-SqlPath -Path $p -SqlServer $SqlServer -SqlCredential $SqlCredential) - { - $BackupFiles += $p - } - else - { - Write-Warning "$FunctionName - $p cannot be accessed by $SqlServer" - } - } - else - { - $BackupFiles += Get-XPDirTreeRestoreFile -Path $p -SqlServer $SqlServer -SqlCredential $SqlCredential - } - } - elseif ((Get-Item $p -ErrorAction SilentlyContinue).PSIsContainer -ne $true) - { - try - { - $BackupFiles += Get-Item $p -ErrorAction Stop - } - catch - { - if (Test-SqlPath -Path $p -SqlServer $SqlServer -SqlCredential $SqlCredential) - { - $BackupFiles += $p - } - else - { - Write-Warning "$FunctionName - $p cannot be accessed by $SqlServer" - continue - } - } - } - elseif ($MaintenanceSolutionBackup) - { - Write-Verbose "$FunctionName : Ola Style Folder" - $BackupFiles += Get-OlaHRestoreFile -Path $p - } - else - { - Write-Verbose "$FunctionName : Standard Directory" - $FileCheck = $BackupFiles.count - $BackupFiles += Get-DirectoryRestoreFile -Path $p - if ((($BackupFiles.count) - $FileCheck) -eq 0) - { - $BackupFiles += Get-OlaHRestoreFile -Path $p - } - } - } - } - elseif (($f -is [System.IO.FileInfo]) -or ($f -is [System.Object] -and $f.FullName.Length -ne 0)) - { - Write-Verbose "$FunctionName : Files passed in $($Path.count)" - Foreach ($FileTmp in $Path) - { - Write-Verbose "$FunctionName - Type - $($FileTmp.GetType()), length =$($FileTmp.length)" - if($FileTmp -is [System.Io.FileInfo] -and $isLocal -eq $False ) - { - Write-Verbose "$FunctionName - File object" - if ($FileTmp.PsIsContainer) - { - $BackupFiles += Get-XPDirTreeRestoreFile -Path $FileTmp.Fullname -SqlServer $SqlServer -SqlCredential $SqlCredential - } - else - { - if (Test-SqlPath -Path $FileTmp.FullName -SqlServer $SqlServer -SqlCredential $SqlCredential) - { - $BackupFiles += $FileTmp - } - else - { - Write-Warning "$FunctionName - $($FileTmp.FullName) cannot be access by $SqlServer" - } - - } - } - elseif(($FileTmp -is [System.Management.Automation.PSCustomObject] )) #Dealing with Pipeline input - { - Write-Verbose "$FunctionName - Should be pipe input " - if ($FileTmp.PSobject.Properties.name -match "Server") - { - #Most likely incoming from Get-DbaBackupHistory - if($Filetmp.Server -ne $SqlServer -and $FileTmp.FullName -notlike '\\*') - { - Write-Warning "$FunctionName - Backups from a different server and on a local drive, can't access" - return - - } - } - if ([bool]($FileTmp.FullName -notmatch '\.\w{3}\Z' )) - { - - foreach ($dir in $Filetmp.path){ - Write-Verbose "$FunctionName - it's a folder, passing to Get-XpDirTree - $($dir)" - $BackupFiles += Get-XPDirTreeRestoreFile -Path $dir -SqlServer $SqlServer -SqlCredential $SqlCredential - } - } - elseif ([bool]($FileTmp.FullName -match '\.\w{3}\Z' )) - { - Write-Verbose "$FunctionName - it's folder" - ForEach ($ft in $Filetmp.FullName) - { - Write-Verbose "$FunctionName - Piped files Test-SqlPath $($ft)" - if (Test-SqlPath -Path $ft -SqlServer $SqlServer -SqlCredential $SqlCredential) - { - $BackupFiles += $ft - } - else - { - Write-Warning "$FunctionName - $($ft) cannot be accessed by $SqlServer" - } - } - - } - } - else - { - Write-Verbose "$FunctionName - Dropped to Default" - $BackupFiles += $FileTmp - } - } - } - } - } - END - { - - - try - { - $Server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential - } - catch - { - Write-Warning "$FunctionName - Cannot connect to $SqlServer" - Return - } - if ($null -ne $DatabaseName) - { - If (($null -ne $Server.Databases[$DatabaseName]) -and ($WithReplace -eq $false)) - { - Write-Warning "$FunctionName - $DatabaseName exists on Sql Instance $SqlServer , must specify WithReplace to continue" - break - } - } - - if ($islocal -eq $false) - { - Write-Verbose "$FunctionName - Remote server, checking folders" - if ($DestinationDataDirectory -ne '') - { - if ((Test-SqlPath -Path $DestinationDataDirectory -SqlServer $SqlServer -SqlCredential $SqlCredential) -ne $true) - { - if ((New-DbaSqlDirectory -Path $DestinationDataDirectory -SqlServer $SqlServer -SqlCredential $SqlCredential).Created -ne $true) - { - Write-Warning "$FunctionName - DestinationDataDirectory $DestinationDataDirectory does not exist, and could not be created on $SqlServer" - break - } - else - { - Write-Verbose "$FunctionName - DestinationDataDirectory $DestinationDataDirectory created on $SqlServer" - } - } - else - { - Write-Verbose "$FunctionName - DestinationDataDirectory $DestinationDataDirectory exists on $SqlServer" - } - } - if ($DestinationLogDirectory -ne '') - { - if ((Test-SqlPath -Path $DestinationLogDirectory -SqlServer $SqlServer -SqlCredential $SqlCredential) -ne $true) - { - if((New-DbaSqlDirectory -Path $DestinationLogDirectory -SqlServer $SqlServer -SqlCredential $SqlCredential).Created -ne $true) - { - Write-Warning "$FunctionName - DestinationLogDirectory $DestinationLogDirectory does not exist, and could not be created on $SqlServer" - break - } - else - { - Write-Verbose "$FunctionName - DestinationLogDirectory $DestinationLogDirectory created on $SqlServer" - } - } - else - { - Write-Verbose "$FunctionName - DestinationLogDirectory $DestinationLogDirectory exists on $SqlServer" - } - } - } -<# - return $BackupFiles - } -} - #> - $AllFilteredFiles = $BackupFiles | Get-FilteredRestoreFile -SqlServer $SqlServer -RestoreTime $RestoreTime -SqlCredential $SqlCredential -IgnoreLogBackup:$IgnoreLogBackup - Write-Verbose "$FunctionName - $($AllFilteredFiles.count) dbs to restore" - - if ($AllFilteredFiles.count -gt 1 -and $DatabaseName -ne '') - { - Write-Warning "$FunctionName - DatabaseName parameter and multiple database restores is not compatible " - break - } - - ForEach ($FilteredFileSet in $AllFilteredFiles) - { - $FilteredFiles = $FilteredFileSet.values - - Write-Verbose "$FunctionName - Starting FileSet" - if (($FilteredFiles.DatabaseName | Group-Object | Measure-Object).count -gt 1) - { - $dbs = ($FilteredFiles | Select-Object -Property DatabaseName) -join (',') - Write-Warning "$FunctionName - We can only handle 1 Database at a time - $dbs" - break - } - - IF ($DatabaseName -eq '') - { - $DatabaseName = $RestoredDatababaseNamePrefix+($FilteredFiles | Select-Object -Property DatabaseName -unique).DatabaseName - Write-Verbose "$FunctionName - Dbname set from backup = $DatabaseName" - } - - if ((Test-DbaLsnChain -FilteredRestoreFiles $FilteredFiles) -and (Test-DbaRestoreVersion -FilteredRestoreFiles $FilteredFiles -SqlServer $SqlServer -SqlCredential $SqlCredential)) - { - try - { - $FilteredFiles | Restore-DBFromFilteredArray -SqlServer $SqlServer -DBName $databasename -SqlCredential $SqlCredential -RestoreTime $RestoreTime -DestinationDataDirectory $DestinationDataDirectory -DestinationLogDirectory $DestinationLogDirectory -NoRecovery:$NoRecovery -Replace:$WithReplace -ScriptOnly:$OutputScriptOnly -FileStructure $FileMapping -VerifyOnly:$VerifyOnly -UseDestinationDefaultDirectories:$UseDestinationDefaultDirectories -ReuseSourceFolderStructure:$ReuseSourceFolderStructure -DestinationFilePrefix $DestinationFilePrefix - - $Completed = 'successfully' - } - catch - { - Write-Exception $_ - $Completed = 'unsuccessfully' - return - } - Finally - { - Write-Verbose "Database $databasename restored $Completed" - } - } - $DatabaseName = '' - } - } -} + .PARAMETER TestBackupInformation + Passing a string value into this parameter will cause a global variable to be created holding the output of Test-DbaBackupInformation + + .PARAMETER StopAfterGetBackupInformation + Switch which will cause the function to exit after returning GetBackupInformation + + .PARAMETER StopAfterSelectBackupInformation + Switch which will cause the function to exit after returning SelectBackupInformation + + .PARAMETER StopAfterFormatBackupInformation + Switch which will cause the function to exit after returning FormatBackupInformation + + .PARAMETER StopAfterTestBackupInformation + Switch which will cause the function to exit after returning TestBackupInformation + + .PARAMETER StatementTimeOut + Timeout in minutes. Defaults to infinity (restores can take a while.) + + .PARAMETER KeepCDC + Indicates whether CDC information should be restored as part of the database + + .PARAMETER PageRestore + Passes in an object from Get-DbaSuspectPages containing suspect pages from a single database. + Setting this Parameter will cause an Online Page restore if the target Instance is Enterprise Edition, or offline if not. + This will involve taking a tail log backup, so you must check your restore chain once it has completed + + .PARAMETER PageRestoreTailFolder + This parameter passes in a location for the tail log backup required for page level restore + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts to confirm certain actions + + .PARAMETER WhatIf + Shows what would happen if the command would execute, but does not actually perform the command + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups + + Scans all the backup files in \\server2\backups, filters them and restores the database to server1\instance1 + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores + + Scans all the backup files in \\server2\backups$ stored in an Ola Hallengren style folder structure, + filters them and restores the database to the c:\restores folder on server1\instance1 + + .EXAMPLE + Get-ChildItem c:\SQLbackups1\, \\server\sqlbackups2 | Restore-DbaDatabase -SqlInstance server1\instance1 + + Takes the provided files from multiple directories and restores them on server1\instance1 + + .EXAMPLE + $RestoreTime = Get-Date('11:19 23/12/2016') + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -MaintenanceSolutionBackup -DestinationDataDirectory c:\restores -RestoreTime $RestoreTime + Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, + filters them and restores the database to the c:\restores folder on server1\instance1 up to 11:19 23/12/2016 + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path \\server2\backups -DestinationDataDirectory c:\restores -OutputScriptOnly | Select-Object -ExpandProperty Tsql | Out-File -Filepath c:\scripts\restore.sql + Scans all the backup files in \\server2\backups stored in an Ola Hallengren style folder structure, + filters them and generate the T-SQL Scripts to restore the database to the latest point in time, + and then stores the output in a file for later retrieval + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path c:\backups -DestinationDataDirectory c:\DataFiles -DestinationLogDirectory c:\LogFile + + Scans all the files in c:\backups and then restores them onto the SQL Server Instance server1\instance1, placing data files + c:\DataFiles and all the log files into c:\LogFiles + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server1\instance1 -Path http://demo.blob.core.windows.net/backups/dbbackup.bak -AzureCredential MyAzureCredential + + Will restore the backup held at http://demo.blob.core.windows.net/backups/dbbackup.bak to server1\instance1. The connection to Azure will be made using the + credential MyAzureCredential held on instance Server1\instance1 + + .EXAMPLE + $File = Get-ChildItem c:\backups, \\server1\backups -recurse + $File | Restore-DbaDatabase -SqlInstance Server1\Instance -useDestinationDefaultDirectories + + This will take all of the files found under the folders c:\backups and \\server1\backups, and pipeline them into + Restore-DbaDatabase. Restore-DbaDatabase will then scan all of the files, and restore all of the databases included + to the latest point in time covered by their backups. All data and log files will be moved to the default SQL Server + folder for those file types as defined on the target instance. + + .EXAMPLE + $files = Get-ChildItem C:\dbatools\db1 + + #Restore database to a point in time + $files | Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -RestoreTime (get-date "14:58:30 22/05/2017") ` + -NoRecovery -WithReplace -StandbyDirectory C:\dbatools\standby + + #It's in standby so we can peek at it + Invoke-Sqlcmd2 -ServerInstance server\instance1 -Query "select top 1 * from Restored.dbo.steps order by dt desc" + + #Not quite there so let's roll on a bit: + $files | Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -continue -WithReplace -RestoreTime (get-date "15:09:30 22/05/2017") ` + -StandbyDirectory C:\dbatools\standby + + Invoke-Sqlcmd2 -ServerInstance server\instance1 -Query "select top 1 * from restored.dbo.steps order by dt desc" + + Restore-DbaDatabase -SqlInstance server\instance1 ` + -DestinationFilePrefix prefix -DatabaseName Restored ` + -continue -WithReplace + + In this example we step through the backup files held in c:\dbatools\db1 folder. + First we restore the database to a point in time in standby mode. This means we can check some details in the databases + We then roll it on a further 9 minutes to perform some more checks + And finally we continue by rolling it all the way forward to the latest point in the backup. + At each step, only the log files needed to roll the database forward are restored. + + .EXAMPLE + Restore-DbaDatabase -SqlInstance server\instance1 -Path c:\backups -DatabaseName example1 -WithNoRecovery + Restore-DbaDatabase -SqlInstance server\instance1 -Recover -DatabaseName example1 + + .EXAMPLE + $SuspectPage = Get-DbaSuspectPage -SqlInstance server\instance1 -Database ProdFinance + Get-DbaBackupHistory - SqlInstance server\instance1 -Database -ProdFinance -Last | Restore-DbaDatabase -PageRestore $SuspectPage -PageRestoreTailFolder c:\temp -TrustDbBackupHistory -AllowContinues + + Gets a list of Suspect Pages using Get-DbaSuspectPage. The uses Get-DbaBackupHistory and Restore-DbaDatabase to perform a restore of the suspect pages and bring them up to date + If server\instance1 is Enterprise edition this will be done online, if not it will be performed offline + AllowContinue is required to make sure we cope with existing files + + .NOTES + Tags: DisasterRecovery, Backup, Restore + Author: Stuart Moore (@napalmgram), stuart-moore.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Restore")] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Restore")] + [parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "RestorePage")] + [object[]]$Path, + [parameter(ValueFromPipeline = $true)] + [Alias("Name")] + [object[]]$DatabaseName, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationDataDirectory, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationLogDirectory, + [parameter(ParameterSetName = "Restore")] + [String]$DestinationFileStreamDirectory, + [parameter(ParameterSetName = "Restore")] + [DateTime]$RestoreTime = (Get-Date).AddYears(1), + [parameter(ParameterSetName = "Restore")] + [switch]$NoRecovery, + [parameter(ParameterSetName = "Restore")] + [switch]$WithReplace, + [parameter(ParameterSetName = "Restore")] + [Switch]$XpDirTree, + [switch]$OutputScriptOnly, + [parameter(ParameterSetName = "Restore")] + [switch]$VerifyOnly, + [parameter(ParameterSetName = "Restore")] + [switch]$MaintenanceSolutionBackup, + [parameter(ParameterSetName = "Restore")] + [hashtable]$FileMapping, + [parameter(ParameterSetName = "Restore")] + [switch]$IgnoreLogBackup, + [parameter(ParameterSetName = "Restore")] + [switch]$useDestinationDefaultDirectories, + [parameter(ParameterSetName = "Restore")] + [switch]$ReuseSourceFolderStructure, + [parameter(ParameterSetName = "Restore")] + [string]$DestinationFilePrefix = '', + [parameter(ParameterSetName = "Restore")] + [Alias("RestoredDatababaseNamePrefix")] + [string]$RestoredDatabaseNamePrefix, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [switch]$TrustDbBackupHistory, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$MaxTransferSize, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$BlockSize, + [parameter(ParameterSetName = "Restore")] + [parameter(ParameterSetName = "RestorePage")] + [int]$BufferCount, + [parameter(ParameterSetName = "Restore")] + [switch]$DirectoryRecurse, + [switch]$EnableException , + [parameter(ParameterSetName = "Restore")] + [string]$StandbyDirectory, + [parameter(ParameterSetName = "Restore")] + [switch]$Continue, + [string]$AzureCredential, + [parameter(ParameterSetName = "Restore")] + [switch]$ReplaceDbNameInFile, + [parameter(ParameterSetName = "Restore")] + [string]$DestinationFileSuffix, + [parameter(ParameterSetName = "Recovery")] + [switch]$Recover, + [parameter(ParameterSetName = "Restore")] + [switch]$KeepCDC, + [switch]$AllowContinue, + [string]$GetBackupInformation, + [switch]$StopAfterGetBackupInformation, + [string]$SelectBackupInformation, + [switch]$StopAfterSelectBackupInformation, + [string]$FormatBackupInformation, + [switch]$StopAfterFormatBackupInformation, + [string]$TestBackupInformation, + [switch]$StopAfterTestBackupInformation, + [parameter(Mandatory = $true, ParameterSetName = "RestorePage")] + [object]$PageRestore, + [parameter(Mandatory = $true, ParameterSetName = "RestorePage")] + [string]$PageRestoreTailFolder, + [int]$StatementTimeout = 0 + + ) + begin { + Write-Message -Level InternalComment -Message "Starting" + Write-Message -Level Debug -Message "Parameters bound: $($PSBoundParameters.Keys -join ", ")" + #[string]$DatabaseName = 'testparam' + #region Validation + try { + $RestoreInstance = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + return + } + if ($PSCmdlet.ParameterSetName -eq "Restore") { + $useDestinationDefaultDirectories = $true + $paramCount = 0 + + if (!(Test-Bound "AllowContinue") -and $true -ne $AllowContinue) { + $AllowContinue = $false + } + if (Test-Bound "FileMapping") { + $paramCount += 1 + } + if (Test-Bound "ReuseSourceFolderStructure") { + $paramCount += 1 + } + if (Test-Bound "DestinationDataDirectory") { + $paramCount += 1 + } + if ($paramCount -gt 1) { + Stop-Function -Category InvalidArgument -Message "You've specified incompatible Location parameters. Please only specify one of FileMapping, ReuseSourceFolderStructure or DestinationDataDirectory" + return + } + if (($ReplaceDbNameInFile) -and !(Test-Bound "DatabaseName")) { + Stop-Function -Category InvalidArgument -Message "To use ReplaceDbNameInFile you must specify DatabaseName" + return + } + + if ((Test-Bound "DestinationLogDirectory") -and (Test-Bound "ReuseSourceFolderStructure")) { + Stop-Function -Category InvalidArgument -Message "The parameters DestinationLogDirectory and UseDestinationDefaultDirectories are mutually exclusive" + return + } + if ((Test-Bound "DestinationLogDirectory") -and -not (Test-Bound "DestinationDataDirectory")) { + Stop-Function -Category InvalidArgument -Message "The parameter DestinationLogDirectory can only be specified together with DestinationDataDirectory" + return + } + if ((Test-Bound "DestinationFileStreamDirectory") -and (Test-Bound "ReuseSourceFolderStructure")) { + Stop-Function -Category InvalidArgument -Message "The parameters DestinationFileStreamDirectory and UseDestinationDefaultDirectories are mutually exclusive" + return + } + if ((Test-Bound "DestinationFileStreamDirectory") -and -not (Test-Bound "DestinationDataDirectory")) { + Stop-Function -Category InvalidArgument -Message "The parameter DestinationFileStreamDirectory can only be specified together with DestinationDataDirectory" + return + } + if (($null -ne $FileMapping) -or $ReuseSourceFolderStructure -or ($DestinationDataDirectory -ne '')) { + $useDestinationDefaultDirectories = $false + } + if (($MaxTransferSize % 64kb) -ne 0 -or $MaxTransferSize -gt 4mb) { + Stop-Function -Category InvalidArgument -Message "MaxTransferSize value must be a multiple of 64kb and no greater than 4MB" + return + } + if ($BlockSize) { + if ($BlockSize -notin (0.5kb, 1kb, 2kb, 4kb, 8kb, 16kb, 32kb, 64kb)) { + Stop-Function -Category InvalidArgument -Message "Block size must be one of 0.5kb,1kb,2kb,4kb,8kb,16kb,32kb,64kb" + return + } + } + if ('' -ne $StandbyDirectory) { + if (!(Test-DbaSqlPath -Path $StandbyDirectory -SqlInstance $RestoreInstance)) { + Stop-Function -Message "$SqlSever cannot see the specified Standby Directory $StandbyDirectory" -Target $SqlInstance + return + } + } + if ($KeepCDC -and ($NoRecovery -or ('' -ne $StandbyDirectory))) { + Stop-Function -Category InvalidArgument -Message "KeepCDC cannot be specified with Norecovery or Standby as it needs recovery to work" + return + } + if ($Continue) { + $ContinuePoints = Get-RestoreContinuableDatabase -SqlInstance $RestoreInstance + #$WithReplace = $true + #$ContinuePoints + } + if (!($PSBoundParameters.ContainsKey("DataBasename"))) { + $PipeDatabaseName = $true + } + + } + + + + # changing statement timeout to $StatementTimeout + if ($StatementTimeout -eq 0) { + Write-Message -Level Verbose -Message "Changing statement timeout to infinity" + } + else { + Write-Message -Level Verbose -Message "Changing statement timeout to ($StatementTimeout) minutes" + } + $RestoreInstance.ConnectionContext.StatementTimeout = ($StatementTimeout * 60) + #endregion Validation + + $isLocal = [dbavalidate]::IsLocalHost($SqlInstance.ComputerName) + + if ($useDestinationDefaultDirectories) { + $DefaultPath = (Get-DbaDefaultPath -SqlInstance $RestoreInstance) + $DestinationDataDirectory = $DefaultPath.Data + $DestinationLogDirectory = $DefaultPath.Log + } + + $BackupHistory = @() + #$useDestinationDefaultDirectories = $true + } + process { + if (Test-FunctionInterrupt) { return } + if ($PSCmdlet.ParameterSetName -like "Restore*") { + if ($PipeDatabaseName -eq $true) {$DatabaseName = ''} + Write-Message -message "ParameterSet = Restore" -Level Verbose + if ($TrustDbBackupHistory -or $path[0].GetType().ToString() -eq 'Sqlcollaborative.Dbatools.Database.BackupHistory') { + foreach ($f in $path) { + Write-Message -Level Verbose -Message "Trust Database Backup History Set" + if ("BackupPath" -notin $f.PSobject.Properties.name) { + Write-Message -Level Verbose -Message "adding BackupPath - $($_.Fullname)" + $f = $f | Select-Object *, @{ Name = "BackupPath"; Expression = { $_.FullName } } + } + if ("DatabaseName" -notin $f.PSobject.Properties.name) { + $f = $f | Select-Object *, @{ Name = "DatabaseName"; Expression = { $_.Database } } + } + if ("Database" -notin $f.PSobject.Properties.name) { + $f = $f | Select-Object *, @{ Name = "Database"; Expression = { $_.DatabaseName } } + } + if ("Type" -notin $f.PSobject.Properties.name) { + #$f = $f | Select-Object *, @{Name="Type";Expression={"Full"}} + } + if ("BackupSetGUID" -notin $f.PSobject.Properties.name) { + #This line until Get-DbaBackupHistory gets fixed + #$f = $f | Select-Object *, @{ Name = "BackupSetGUID"; Expression = { $_.BackupSetupID } } + #This one once it's sorted: + $f = $f | Select-Object *, @{Name = "BackupSetGUID"; Expression = {$_.BackupSetID}} + } + if ($f.BackupPath -like 'http*' -and '' -eq $AzureCredential) { + Stop-Function -Message "At least one Azure backup passed in, and no Credential supplied. Stopping" + return + } + + $BackupHistory += $F | Select-Object *, @{ Name = "ServerName"; Expression = { $_.SqlInstance } }, @{ Name = "BackupStartDate"; Expression = { $_.Start -as [DateTime] } } + + } + } + else { + $files = @() + foreach ($f in $Path) { + if ($f -is [System.IO.FileSystemInfo]) { + $files += $f.fullname + } + else { + $files += $f + } + } + Write-Message -Level Verbose -Message "Unverified input, full scans - $($files -join ';')" + $BackupHistory += Get-DbaBackupInformation -SqlInstance $RestoreInstance -SqlCredential $SqlCredential -Path $files -DirectoryRecurse:$DirectoryRecurse -MaintenanceSolution:$MaintenanceSolutionBackup -IgnoreLogBackup:$IgnoreLogBackup -AzureCredential $AzureCredential + } + if ($PSCmdlet.ParameterSetName -eq "RestorePage") { + if (-not (Test-DbaSqlPath -SqlInstance $RestoreInstance -Path $PageRestoreTailFolder)) { + Stop-Function -Message "Instance $RestoreInstance cannot read $PageRestoreTailFolder, cannot proceed" -Target $PageRestoreTailFolder + return + } + $WithReplace = $true + } + } + elseif ($PSCmdlet.ParameterSetName -eq "Recovery") { + Write-Message -Message "$($Database.count) databases to recover" -level Verbose + ForEach ($DataBase in $DatabaseName) { + if ($database -is [object]) { + #We've got an object, try the normal options Database, DatabaseName, Name + if ("Database" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.Database + } + elseif ("DatabaseName" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.DatabaseName + } + elseif ("Name" -in $Database.PSobject.Properties.name) { + [string]$DataBase = $database.name + } + } + Write-Verbose "existence - $($RestoreInstance.Databases[$DataBase].State)" + if ($RestoreInstance.Databases[$DataBase].State -ne 'Existing') { + Write-Message -Message "$Database does not exist on $RestoreInstance" -level Warning + Continue + + } + if ($RestoreInstance.Databases[$Database].Status -ne "Restoring") { + Write-Message -Message "$Database on $RestoreInstance is not in a Restoring State" -Level Warning + Continue + + } + $RestoreComplete = $true + $RecoverSql = "RESTORE DATABASE $Database WITH RECOVERY" + Write-Message -Message "Recovery Sql Query - $RecoverSql" -level verbose + Try { + $RestoreInstance.query($RecoverSql) + } + Catch { + $RestoreComplete = $False + $ExitError = $_.Exception.InnerException + Write-Message -Level Warning -Message "Failed to recover $Database on $RestoreInstance, `n $ExitError" + } + Finally { + [PSCustomObject]@{ + SqlInstance = $SqlInstance + DatabaseName = $Database + RestoreComplete = $RestoreComplete + Scripts = $RecoverSql + } + } + } + } + } + end { + if (Test-FunctionInterrupt) { return } + if ($PSCmdlet.ParameterSetName -like "Restore*") { + if ($BackupHistory.Count -eq 0) { + Write-Message -Level Warning -Message "No backups passed through. `n This could mean the SQL instance cannot see the referenced files, the file's headers could not be read or some other issue" + return + } + Write-Message -message "Processing DatabaseName - $DatabaseName" -Level Verbose + $FilteredBackupHistory = @() + if (Test-Bound -ParameterName GetBackupInformation) { + Write-Message -Message "Setting $GetBackupInformation to BackupHistory" -Level Verbose + Set-Variable -Name $GetBackupInformation -Value $BackupHistory -Scope Global + } + if ($StopAfterGetBackupInformation) { + return + } + + $FilteredBackupHistory = $BackupHistory | Select-DbaBackupInformation -RestoreTime $RestoreTime -IgnoreLogs:$IgnoreLogBackups -ContinuePoints $ContinuePoints + + if ( Test-Bound -ParameterName SelectBackupInformation) { + Write-Message -Message "Setting $SelectBackupInformation to FilteredBackupHistory" -Level Verbose + Set-Variable -Name $SelectBackupInformation -Value $FilteredBackupHistory -Scope Global + + } + if ($StopAfterSelectBackupInformation) { + return + } + + $null = $FilteredBackupHistory | Format-DbaBackupInformation -DataFileDirectory $DestinationDataDirectory -LogFileDirectory $DestinationLogDirectory -DestinationFileStreamDirectory $DestinationFileStreamDirectory -DatabaseFileSuffix $DestinationFileSuffix -DatabaseFilePrefix $DestinationFilePrefix -DatabaseNamePrefix $RestoredDatabaseNamePrefix -ReplaceDatabaseName $DatabaseName -Continue:$Continue -ReplaceDbNameInFile:$ReplaceDbNameInFile -FileMapping $FileMapping + + if ( Test-Bound -ParameterName FormatBackupInformation) { + Set-Variable -Name $FormatBackupInformation -Value $FilteredBackupHistory -Scope Global + } + if ($StopAfterFormatBackupInformation) { + return + } + + try { + Write-Message -Level Verbose -Message "VerifyOnly = $VerifyOnly" + $null = $FilteredBackupHistory | Test-DbaBackupInformation -SqlInstance $RestoreInstance -WithReplace:$WithReplace -Continue:$Continue -VerifyOnly:$VerifyOnly -EnableException:$true + } + catch { + Stop-Function -ErrorRecord $_ -Message "Failure" -Continue + } + + if ( Test-Bound -ParameterName TestBackupInformation) { + Set-Variable -Name $TestBackupInformation -Value $FilteredBackupHistory -Scope Global + } + if ($StopAfterTestBackupInformation) { + return + } + $DbVerfied = ($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $True} | Select-Object -Property Database -Unique).Database -join ',' + Write-Message -Message "$DbVerfied passed testing" -Level Verbose + + if (($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $True}).count -lt $FilteredBackupHistory.count) { + $DbUnVerified = ($FilteredBackupHistory | Where-Object {$_.IsVerified -eq $False} | Select-Object -Property Database -Unique).Database -join ',' + if ($AllowContinue) { + Write-Message -Message "$DbUnverified failed testing, AllowContinue set" -Level Verbose + } + else { + Stop-Function -Message "Database $DbUnverified failed testing, AllowContinue not set, exiting" + return + } + } + + If ($PSCmdlet.ParameterSetName -eq "RestorePage") { + if (($FilteredBackupHistory.Database | select-Object -unique | Measure-Object).count -ne 1) { + Stop-Function -Message "Must only 1 database passed in for Page Restore. Sorry" + return + } + else { + $WithReplace = $false + $PageDb = ($FilteredBackupHistory.Database | select-Object -unique).Database + } + } + Write-Message -Message "Passing in to restore" -Level Verbose + if ($PSCmdlet.ParameterSetName -eq "RestorePage" -and $RestoreInstance.Edition -notlike '*Enterprise*') { + Write-Message -Message "Taking Tail log backup for page restore for non-Enterprise" -Level Verbose + $TailBackup = Backup-DbaDatabase -SqlInstance $RestoreInstance -Database $DatabaseName -Type Log -BackupDirectory $PageRestoreTailFolder -Norecovery -CopyOnly + } + $FilteredBackupHistory | Where-Object {$_.IsVerified -eq $true} | Invoke-DbaAdvancedRestore -SqlInstance $RestoreInstance -WithReplace:$WithReplace -RestoreTime $RestoreTime -StandbyDirectory $StandbyDirectory -NoRecovery:$NoRecovery -Continue:$Continue -OutputScriptOnly:$OutputScriptOnly -BlockSize $BlockSize -MaxTransferSize $MaxTransferSize -Buffercount $Buffercount -KeepCDC:$KeepCDC -VerifyOnly:$VerifyOnly -PageRestore $PageRestore + if ($PSCmdlet.ParameterSetName -eq "RestorePage" ) { + if ($RestoreInstace.Edition -like '*Enterprise*') { + Write-Message -Message "Taking Tail log backup for page restore for Enterprise" -Level Verbose + $TailBackup = Backup-DbaDatabase -SqlInstance $RestoreInstance -Database $DatabaseName -Type Log -BackupDirectory $PageRestoreTailFolder -Norecovery -CopyOnly + } + Write-Message -Message "Restoring Tail log backup for page restore" -Level Verbose + $TailBackup | Restore-DbaDatabase -SqlInstance $RestoreInstance -TrustDbBackupHistory -NoRecovery -OutputScriptOnly:$OutputScriptOnly -BlockSize $BlockSize -MaxTransferSize $MaxTransferSize -Buffercount $Buffercount -Continue + Restore-DbaDatabase -SqlInstance $RestoreInstance -Recover -Database $DatabaseName -OutputScriptOnly:$OutputScriptOnly + } + + } + } +} diff --git a/functions/Restore-DbaDbCertificate.ps1 b/functions/Restore-DbaDbCertificate.ps1 new file mode 100644 index 000000000000..b7df1439489f --- /dev/null +++ b/functions/Restore-DbaDbCertificate.ps1 @@ -0,0 +1,117 @@ +function Restore-DbaDbCertificate { + <# +.SYNOPSIS +Imports certificates from .cer files using SMO. + +.DESCRIPTION +Imports certificates from.cer files using SMO. + +.PARAMETER SqlInstance +The SQL Server to create the certificates on. + +.PARAMETER Path +The Path the contains the certificate and private key files. The path can be a directory or a specific certificate. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Password +Secure string used to decrypt the private key. + +.PARAMETER Database +The database where the certificate imports into. Defaults to master. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Tags: Migration, Certificate +Author: Jess Pomfret (@jpomfret) + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Restore-DbaDbCertificate -SqlInstance Server1 -Path \\Server1\Certificates -password (ConvertTo-SecureString -force -AsPlainText GoodPass1234!!) +Imports all the certificates in the specified path. + +#> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Path, + [object]$Database = "master", + [Security.SecureString]$Password = (Read-Host "Password" -AsSecureString), + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Retore-DbaDatabaseCertificate + + function new-smocert ($directory, $certname) { + if ($Pscmdlet.ShouldProcess("$cert on $SqlInstance", "Importing Certificate")) { + $smocert = New-Object Microsoft.SqlServer.Management.Smo.Certificate + $smocert.Name = $certname + $smocert.Parent = $server.Databases[$Database] + Write-Message -Level Verbose -Message "Creating Certificate: $certname" + try { + $fullcertname = "$directory\$certname.cer" + $privatekey = "$directory\$certname.pvk" + Write-Message -Level Verbose -Message "Full certificate path: $fullcertname" + Write-Message -Level Verbose -Message "Private key: $privatekey" + $fromfile = 1 + $smocert.Create($fullcertname, $fromfile, $privatekey, [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)), [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password))) + $smocert + } + catch { + Write-Message -Level Warning -Message $_ -ErrorRecord $_ -Target $instance + } + } + } + + try { + Write-Message -Level Verbose -Message "Connecting to $SqlInstance" + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failed to connect to: $SqlInstance" -Target $SqlInstance -InnerErrorRecord $_ + return + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($fullname in $path) { + + if (-not $SqlInstance.IsLocalHost -and -not $fullname.StartsWith('\')) { + Stop-Function -Message "Path ($fullname) must be a UNC share when SQL instance is not local." -Continue -Target $fullname + } + + if (-not (Test-DbaSqlPath -SqlInstance $server -Path $fullname)) { + Stop-Function -Message "$SqlInstance cannot access $fullname" -Continue -Target $fullname + } + + $directory = Split-Path $fullname + $filename = Split-Path $fullname -Leaf + $basename = [io.path]::GetFileNameWithoutExtension($filename) + $cert = new-smocert -directory $directory -certname $basename + Get-DbaDbCertificate -SqlInstance $server -Database $Database -Certificate $cert.Name + } + } +} \ No newline at end of file diff --git a/functions/Restore-DbaDbSnapshot.ps1 b/functions/Restore-DbaDbSnapshot.ps1 new file mode 100644 index 000000000000..4caed4e1cc16 --- /dev/null +++ b/functions/Restore-DbaDbSnapshot.ps1 @@ -0,0 +1,329 @@ +#ValidationTags#FlowControl# +function Restore-DbaDbSnapshot { + <# + .SYNOPSIS + Restores databases from snapshots + + .DESCRIPTION + Restores the database from the snapshot, discarding every modification made to the database + NB: Restoring to a snapshot will result in every other snapshot of the same database to be dropped + It also fixes some long-standing bugs in SQL Server when restoring from snapshots + + .PARAMETER SqlInstance + The SQL Server that you're connecting to + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user + + .PARAMETER Database + Restores from the last snapshot databases with this names only. You can pass either Databases or Snapshots + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER Snapshot + Restores databases from snapshots with this names only. You can pass either Databases or Snapshots + + .PARAMETER Force + If restoring from a snapshot involves dropping any other shapshot, you need to explicitly + use -Force to let this command delete the ones not involved in the restore process. + Also, -Force will forcibly kill all running queries that prevent the restore process. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Snapshot, Backup, Restore, Database + Author: niphlod + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Restore-DbaDbSnapshot + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR, Accounting + + Restores HR and Accounting databases using the latest snapshot available + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Database HR -Force + + Restores HR database from latest snapshot and kills any active connections in the database on sqlserver2014a. + + .EXAMPLE + Restore-DbaDbSnapshot -SqlInstance sqlserver2014a -Snapshot HR_snap_20161201, Accounting_snap_20161101 + + Restores databases from snapshots named HR_snap_20161201 and Accounting_snap_20161101 + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [object[]]$Snapshot, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + if (!$Snapshot -and !$Database -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify either -Snapshot (to restore from) or -Database/-ExcludeDatabase (to restore to)" + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + #$allDbs = $server.Databases + $alldbq = @" +SELECT sn.name AS Name, + dt.name AS DatabaseSnapshotBaseName, + sn.create_date AS CreateDate, + CASE + WHEN sn.source_database_id IS NOT NULL + THEN CASE + WHEN dt.state = 6 + THEN 0 + ELSE 1 + END + ELSE CASE + WHEN sn.state = 6 + THEN 0 + ELSE 1 + END + END AS IsAccessible, + CASE + WHEN sn.source_database_id IS NOT NULL + THEN 1 + ELSE 0 + END AS IsDatabaseSnapshot +FROM sys.databases sn + LEFT JOIN sys.databases dt ON sn.source_database_id = dt.database_id +"@ + $alldbs = $server.Query($alldbq) + # vault to hold all programmed operations from --> to + $operations = @() + + if (!$Snapshot -and !$Database -and !$ExcludeDatabase) { + # Restore all databases from the latest snapshot + Write-Message -Level Verbose -Message "Selected all databases" + $dbs = $allDbs | Where-Object IsDatabaseSnapshot -EQ $true + } + elseif ($Database) { + # Restore only these databases from their latest snapshot + <# + Note, for some reason lookup of this property has to be done using $_.DatabaseSnapshotBaseName + Removing this will cause: Where-Object : A positional parameter cannot be found that accepts argument 'System.Object[]' + #> + Write-Message -Level Verbose -Message "Selected only databases" + $dbs = $allDbs | Where-Object {$Database -contains $_.DatabaseSnapshotBaseName} + } + elseif ($ExcludeDatabase) { + Write-Message -Level Verbose -Message "Excluded only databases" + $dbs = $allDbs | Where-Object {$ExcludeDatabase -NotContains $_.DatabaseSnapshotBaseName} + } + elseif ($Snapshot) { + # Restore databases from these snapshots + Write-Message -Level Verbose -Message "Selected only snapshots" + $dbs = $allDbs | Where-Object { $Snapshot -contains $_.Name } + $baseDatabases = $dbs | Select-Object -ExpandProperty DatabaseSnapshotBaseName | Get-Unique + if ($baseDatabases.Count -ne $Snapshot.Count -and $dbs.Count -ne 0) { + Write-Message -Level Warning -Message "Multiple snapshots selected for the same database, skipping" + continue + } + } + $opsHash = @{ } + foreach ($db in $dbs) { + if (-not($db.IsAccessible)) { + Write-Message -Level Warning -Message "Database $db is not accessible." + continue + } + if ($db.DatabaseSnapshotBaseName -notin $opsHash.Keys) { + if ($snapshot.Count -gt 0) { + # just in the need to drop every other snapshot + $toDrop = $allDbs | Where-Object { $_.DatabaseSnapshotBaseName -eq $db.DatabaseSnapshotBaseName } + $toDrop = $todrop | Select-Object Name + $opsHash[$db.DatabaseSnapshotBaseName] = @{ + 'from' = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate + 'drop' = $toDrop + } + } + else { + $opsHash[$db.DatabaseSnapshotBaseName] = @{ + 'from' = $db + 'drop' = @() + } + } + } + else { + # store each older snapshot in the drop list while enumerating + if ($db.CreateDate -gt $opsHash[$db.DatabaseSnapshotBaseName]['from'].CreateDate) { + $prev = $opsHash[$db.DatabaseSnapshotBaseName]['from'] + $opsHash[$db.DatabaseSnapshotBaseName]['from'] = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate + $opsHash[$db.DatabaseSnapshotBaseName]['drop'] += $prev + } + } + } + foreach ($dbName in $opsHash.Keys) { + $drop = @() + foreach ($toDrop in $opsHash[$dbName]['drop']) { + $drop += $toDrop.Name + } + $operations += @{ + 'from' = $opsHash[$dbName]['from'].Name + 'to' = $dbName + 'drop' = $drop + } + } + foreach ($op in $operations) { + # Check if there are FS, because then a restore is not possible + $all_FS = $server.Databases[$op['to']].FileGroups | Where-Object FileGroupType -EQ 'FileStreamDataFileGroup' + if ($all_FS.Count -gt 0) { + Write-Message -Level Warning -Message "Database $($op['to']) has FileStream group(s). You cannot restore from snapshots" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = "Database $($op['to']) has FileStream group(s). You cannot restore from snapshots" + } + break + } + # Get log size and autogrowth + $orig_logproperties = $server.Databases[$op['to']].LogFiles | Select-Object Id, Size, Growth, GrowthType + # Drop what needs to be dropped + $opError = $false + + if ($op['drop'].Count -gt 1 -and $Force -eq $false) { + $warnMsg = @() + $warnMsg += "The restore process for $($op['to']) from $($op['from']) needs to drop the following:" + foreach ($db in $op['drop']) { + $warnMsg += $db + } + $warnMsg += "Use -Force if you really want to drop these snapshots." + Write-Message -Level Warning -Message ($warnMsg -join "`n") + break + } + foreach ($drop in $op['drop']) { + if ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $drop")) { + # skip it if it's the same name + if ($drop -ne $($op['from'])) { + try { + if ($Force) { + # snapshot with open transactions cannot be dropped + $server.KillAllProcesses($drop) + } + $null = $server.Query("USE master; DROP DATABASE [$drop]") + $status = "Dropped" + } + catch { + Write-Message -Level Warning -Message $_ + $operror = $true + break + } + } + } + } + if ($opError) { + Write-Message -Level Warning -Message "Errors trying to restore $($op['to']) from $($op['from'])" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = "Failed to drop some snapshots" + } + break + } + + # Need a proper restore now + if ($Pscmdlet.ShouldProcess($server.DomainInstanceName, "Restore db $($op['to']) from $($op['from'])")) { + $query = "USE master; RESTORE DATABASE [$($op['to'])] FROM DATABASE_SNAPSHOT='$($op['from'])'" + try { + if ($Force) { + # for whatever reason, a snapshot with open transactions, albeit read-only, block the restore process + $server.KillAllProcesses($op['from']) + # for a "good" reason, all open transactions on the destination block the restore process + $server.KillAllProcesses($op['to']) + } + $null = $server.Query($query) + } + catch { + $opError = $true + $inner = $_.Exception.Message + Stop-Function -Message "Original exception: $inner, Query issued: $query, Error: $_.Exception.InnerException.InnerException.Message" -ErrorRecord $_ + } + } + if ($operror) { + Write-Message -Level Warning -Message "Errors trying to restore $($op['to']) from $($op['from'])" + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Error' + Notes = '' + } + break + } + # Comparing sizes before and after, need to refresh to see if size + foreach ($log in $server.Databases[$op['to']].LogFiles) { + $log.Refresh() + } + foreach ($log in $server.Databases[$op['to']].LogFiles) { + $matching = $orig_logproperties | Where-Object ID -EQ $log.ID + $changeflag = 0 + foreach ($prop in @('Size', 'Growth', 'Growth', 'GrowthType')) { + if ($matching.$prop -ne $log.$prop) { + $changeflag = 1 + $log.$prop = $matching.$prop + } + } + if ($changeflag -ne 0) { + Write-Message -Level Verbose -Message "Restoring original settings for log file" + $log.Alter() + } + } + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $op['to'] + Snapshot = $op['from'] + Status = 'Restored' + Notes = 'Remember to take a backup now, and also to remove the snapshot if not needed' + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -Alias Restore-DbaFromDatabaseSnapshot + } +} \ No newline at end of file diff --git a/functions/Restore-DbaFromDatabaseSnapshot.ps1 b/functions/Restore-DbaFromDatabaseSnapshot.ps1 deleted file mode 100644 index f0d2828b0dbc..000000000000 --- a/functions/Restore-DbaFromDatabaseSnapshot.ps1 +++ /dev/null @@ -1,290 +0,0 @@ -Function Restore-DbaFromDatabaseSnapshot -{ -<# -.SYNOPSIS -Restores databases from snapshots - -.DESCRIPTION -Restores the database from the snapshot, discarding every modification made to the database -NB: Restoring to a snapshot will result in every other snapshot of the same database to be dropped -It also fixes some long-standing bugs in SQL Server when restoring from snapshots - -.PARAMETER SqlInstance -The SQL Server that you're connecting to - -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user - -.PARAMETER Databases -Restores from the last snapshot databases with this names only -NB: you can pass either Databases or Snapshots - -.PARAMETER Snapshots -Restores databases from snapshots with this names only -NB: you can pass either Databases or Snapshots - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER Force -If restoring from a snapshot involves dropping any other shapshot, you need to explicitly -use -Force to let this command delete the ones not involved in the restore process. - -.NOTES -Tags: DisasterRecovery, Snapshot, Backup, Restore -Author: niphlod - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK - https://dbatools.io/Restore-DbaFromDatabaseSnapshot - -.EXAMPLE -Restore-DbaFromDatabaseSnapshot -SqlServer sqlserver2014a -Databases HR, Accounting - -Restores HR and Accounting databases using the latest snapshot available - -.EXAMPLE -Restore-DbaFromDatabaseSnapshot -SqlServer sqlserver2014a -Snapshots HR_snap_20161201, Accounting_snap_20161101 - -Restores databases from snapshots named HR_snap_20161201 and Accounting_snap_20161101 - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [string[]]$SqlInstance, - [PsCredential]$Credential, - [switch]$Force - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlSnapshotsAndDatabases -SqlServer $SqlInstance[0] -SqlCredential $Credential - } - } - - BEGIN - { - $databases = $psboundparameters.Databases - $snapshots = $psboundparameters.Snapshots - } - - PROCESS - { - if ($snapshots.count -eq 0 -and $databases.count -eq 0) - { - Write-Warning "You must specify either -Snapshots (to restore from) or -Databases (to restore to)" - return - } - - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - } - catch - { - Write-Warning "Can't connect to $instance" - Continue - } - - $all_dbs = $server.Databases - - # vault to hold all programmed operations from --> to - $operations = @() - - if ($snapshots.count -eq 0 -and $databases.count -eq 0) - { - # Restore all databases from the latest snapshot - Write-Verbose "Selected all databases" - $dbs = $all_dbs | Where-Object IsDatabaseSnapshot -eq $true - } - elseif ($databases.count -gt 0) - { - # Restore only these databases from their latest snapshot - Write-Verbose "Selected only databases" - $dbs = $all_dbs | Where-Object { $databases -contains $_.DatabaseSnapshotBaseName } - } - elseif ($snapshots.count -gt 0) - { - # Restore databases from these snapshots - Write-Verbose "Selected only snapshots" - $dbs = $all_dbs | Where-Object { $snapshots -contains $_.Name } - $base_databases = $dbs | Select-Object -ExpandProperty DatabaseSnapshotBaseName | Get-Unique - if($base_databases.count -ne $snapshots.count) - { - Write-Warning "Multiple snapshots selected for the same database, skipping" - Continue - } - } - - $opshash = @{} - - foreach($db in $dbs) - { - if($db.DatabaseSnapshotBaseName -notin $opshash.Keys) { - if($snapshots.count -gt 0) - { - # just in the need to drop every other snapshot - $todrop = $all_dbs | Where-Object {$_.DatabaseSnapshotBaseName -eq $db.DatabaseSnapshotBaseName} - $todrop = $todrop | Select-Object Name - $opshash[$db.DatabaseSnapshotBaseName] = @{ - 'from' = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate - 'drop' = $todrop - } - } - else - { - $opshash[$db.DatabaseSnapshotBaseName] = @{ - 'from' = $db - 'drop' = @() - } - } - } - else - { - # store each older snapshot in the drop list while enumerating - if($db.createDate -gt $opshash[$db.DatabaseSnapshotBaseName]['from'].CreateDate) { - $prev = $opshash[$db.DatabaseSnapshotBaseName]['from'] - $opshash[$db.DatabaseSnapshotBaseName]['from'] = $db | Select-Object Name, DatabaseSnapshotBaseName, CreateDate - $opshash[$db.DatabaseSnapshotBaseName]['drop'] += $prev - } - } - } - foreach($dbname in $opshash.Keys) - { - $drop = @() - foreach($todrop in $opshash[$dbname]['drop']) - { - $drop += $todrop.Name - } - $operations += @{ - 'from' = $opshash[$dbname]['from'].Name; - 'to' = $dbname; - 'drop' = $drop - } - } - - foreach($op in $operations) - { - # Check if there are FS, because then a restore is not possible - $all_FS = $server.Databases[$op['to']].FileGroups | Where-Object FileGroupType -eq 'FileStreamDataFileGroup' - if($all_FS.Count -gt 0) - { - Write-Warning "Database '$($op['to'])' has FileStream group(s). You cannot restore from snapshots" - [PSCustomObject]@{ - Server = $Server.Name - Database = $op['to'] - Status = 'Error' - Notes = "Database '$($op['to'])' has FileStream group(s). You cannot restore from snapshots" - } - break - } - # Get log size and autogrowth - $orig_logproperties = $server.Databases[$op['to']].LogFiles | Select-Object id, size - # Drop what needs to be dropped - $operror = $false - - if($op['drop'].count -gt 1 -and $Force -eq $false) - { - Write-Warning "The restore process for '$($op['to'])' from '$($op['from'])' needs to drop the following:" - foreach($db in $op['drop']) { - Write-Warning $db - } - Write-Warning "Use -Force if you really want to drop these snapshots." - break - } - foreach($drop in $op['drop']) - { - If ($Pscmdlet.ShouldProcess($server.name, "Remove db snapshot $drop")) - { - # SKIP IT IF IT'S THE SAME NAME - if ($drop -ne $($op['from'])) - { - $dropped = Remove-SqlDatabase -SqlServer $server -DBName $drop -SqlCredential $Credential - if ($dropped -notmatch "Success") - { - Write-Warning $dropped - $operror = $true - break - } - } - } - } - if($operror) - { - Write-Warning "Errors trying to restore '$($op['to'])' from '$($op['from'])'" - [PSCustomObject]@{ - Server = $Server.Name - Database = $op['to'] - Status = 'Error' - Notes = "Failed to drop some snapshots" - } - break - } - - # Need a proper restore now - If ($Pscmdlet.ShouldProcess($server.name, "Restore db '$($op['to'])' from '$($op['from'])'")) - { - $query = "RESTORE DATABASE [$($op['to'])] FROM DATABASE_SNAPSHOT='$($op['from'])'" - try - { - $server.KillAllProcesses($op['to']) - $server.ConnectionContext.ExecuteScalar($query) - } - catch - { - $operror = $true - Write-Exception $_ - $inner = $_.Exception.Message - Write-Warning "Original exception: $inner, Query issued: $query" - } - } - if($operror) - { - Write-Warning "Errors trying to restore '$($op['to'])' from '$($op['from'])'" - [PSCustomObject]@{ - Server = $Server.Name - Database = $op['to'] - Status = 'Error' - Notes = '' - } - break - } - # Comparing sizes before and after, need to reconnect to see if size - # changed - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - foreach($log in $server.Databases[$op['to']].LogFiles) - { - $matching = $orig_logproperties | Where-Object ID -eq $log.ID - if($matching.Size -ne $orig_logproperties.Size) - { - Write-Verbose "Resizing log to the original value" - $log.Size = $matching.Size - $log.Alter() - } - } - [PSCustomObject]@{ - Server = $Server.Name - Database = $op['to'] - Status = 'Restored' - Notes = 'Remember to take a backup now, and also to remove the snapshot if not needed' - } - } - } - } -} - diff --git a/functions/Restore-SqlBackupFromDirectory.ps1 b/functions/Restore-SqlBackupFromDirectory.ps1 deleted file mode 100644 index dee31bc1d05c..000000000000 --- a/functions/Restore-SqlBackupFromDirectory.ps1 +++ /dev/null @@ -1,382 +0,0 @@ -Function Restore-SqlBackupFromDirectory -{ -<# -.SYNOPSIS -Restores SQL Server databases from the backup directory structure created by Ola Hallengren's database maintenance scripts. Different structures coming soon. - -.DESCRIPTION -Many SQL Server database administrators use Ola Hallengren's SQL Server Maintenance Solution which can be found at http://ola.hallengren.com -Hallengren uses a predictable backup structure which made it relatively easy to create a script that can restore an entire SQL Server database instance, down to the master database (next version), to a new server. This script is intended to be used in the event that the originating SQL Server becomes unavailable, thus rendering my other SQL restore script (http://goo.gl/QmfQ6s) ineffective. - -.PARAMETER SqlServer -Required. The SQL Server to which you will be restoring the databases. - -.PARAMETER Path -Required. The directory that contains the database backups (ex. \\fileserver\share\sqlbackups\SQLSERVERA) - -.PARAMETER ReuseSourceFolderStructure -Restore-SqlBackupFromDirectory will restore to the default user data and log directories, unless this switch is used. Useful if you're restoring from a server that had a complex db file structure. - -.PARAMETER Databases -Migrates ONLY specified databases. This list is auto-populated for tab completion. - -.PARAMETER Exclude -Excludes specified databases from migration. This list is auto-populated for tab completion. - -.PARAMETER Force -Will overwrite any existing databases on $SqlServer. - -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - -$cred = Get-Credential, this pass this $cred to the param. - -Windows Authentication will be used if DestinationSqlCredential is not specified. To connect as a different Windows user, run PowerShell as that user. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER NoRecovery -Leaves the databases in No Recovery state to enable further backups to be added - -.NOTES -Tags: DisasterRecovery, Backup, Restore -Author : Chrissy LeMaire, netnerds.net -Requires: sysadmin access on destination SQL Server. - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - - -.LINK -https://dbatools.io/Restore-SqlBackupFromDirectory - -.EXAMPLE -Restore-SqlBackupFromDirectory -ServerName sqlcluster -Path \\fileserver\share\sqlbackups\SQLSERVER2014A - -Description - -All user databases contained within \\fileserver\share\sqlbackups\SQLSERVERA will be restored to sqlcluster, down the most recent full/differential/logs. - -#> - #Requires -Version 3.0 - [CmdletBinding()] - Param ( - [parameter(Mandatory = $true)] - [Alias("ServerInstance","SqlInstance")] - [string]$SqlServer, - [parameter(Mandatory = $true)] - [string]$Path, - [switch]$NoRecovery, - [Alias("ReuseFolderStructure")] - [switch]$ReuseSourceFolderStructure, - [System.Management.Automation.PSCredential]$SqlCredential, - [switch]$Force - - ) - - DynamicParam - { - - if ($Path) - { - $newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary - $paramattributes = New-Object System.Management.Automation.ParameterAttribute - $paramattributes.ParameterSetName = "__AllParameterSets" - $paramattributes.Mandatory = $false - $systemdbs = @("master", "msdb", "model", "SSIS") - $dblist = (Get-ChildItem -Path $Path -Directory).Name | Where-Object { $systemdbs -notcontains $_ } - $argumentlist = @() - - foreach ($db in $dblist) - { - $argumentlist += [Regex]::Escape($db) - } - - $validationset = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $argumentlist - $combinedattributes = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] - $combinedattributes.Add($paramattributes) - $combinedattributes.Add($validationset) - $Databases = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Databases", [String[]], $combinedattributes) - $Exclude = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Exclude", [String[]], $combinedattributes) - $newparams.Add("Databases", $Databases) - $newparams.Add("Exclude", $Exclude) - return $newparams - } - } - - BEGIN { - - Function Restore-Database - { - <# - .SYNOPSIS - Restores .bak file to SQL database. Creates db if it doesn't exist. $filestructure is - a custom object that contains logical and physical file locations. - - .EXAMPLE - $filestructure = Get-SqlFileStructure $sourceserver $destserver $ReuseFolderstructure - Restore-Database $destserver $dbname $backupfile $filetype - - .OUTPUTS - $true if success - $true if failure - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [object]$server, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$dbname, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$backupfile, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$filetype, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [object]$filestructure - - ) - - $servername = $server.name - $server.ConnectionContext.StatementTimeout = 0 - $restore = New-Object "Microsoft.SqlServer.Management.Smo.Restore" - $restore.ReplaceDatabase = $true - - foreach ($file in $filestructure.values) - { - $movefile = New-Object "Microsoft.SqlServer.Management.Smo.RelocateFile" - $movefile.LogicalFileName = $file.logical - $movefile.PhysicalFileName = $file.physical - $null = $restore.RelocateFiles.Add($movefile) - } - - try - { - - $percent = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete $_.Percent -status ([System.String]::Format("Progress: {0} %", $_.Percent)) - } - $restore.add_PercentComplete($percent) - $restore.PercentCompleteNotification = 1 - $restore.add_Complete($complete) - $restore.ReplaceDatabase = $true - $restore.Database = $dbname - $restore.Action = $filetype - $restore.NoRecovery = $true - $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem - $device.name = $backupfile - $device.devicetype = "File" - $restore.Devices.Add($device) - - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -percentcomplete 0 -status ([System.String]::Format("Progress: {0} %", 0)) - $restore.sqlrestore($server) - Write-Progress -id 1 -activity "Restoring $dbname to $servername" -status "Complete" -Completed - - return $true - } - catch - { - $x = $_.Exception - Write-Warning "Restore failed: $x" - return $false - } - } - - - if (!([string]::IsNullOrEmpty($Path))) - { - if (!($Path.StartsWith("\\"))) - { - throw "Path must be a valid UNC path (\\server\share)." - } - - if (!(Test-Path $Path)) - { - throw "$Path does not exist or cannot be accessed." - } - } - - Write-Output "Attempting to connect to SQL Server.." - $server = Connect-SqlServer -SqlServer $SqlServer -SqlCredential $SqlCredential - $server.ConnectionContext.StatementTimeout = 0 - - if ($server.versionMajor -lt 8 -and $server.versionMajor -lt 8) - { - throw "This script can only be run on SQL Server 2000 and above. Quitting." - } - - if ($server.versionMajor -eq 8 -and $IncludeSystemDbs) - { - throw "Migrating system databases not supported in SQL Server 2000." - } - - # Convert from RuntimeDefinedParameter object to regular array - $Databases = $psboundparameters.Databases - $Exclude = $psboundparameters.Exclude - - } - PROCESS - - { - - $dblist = @(); $skippedb = @{ }; $migrateddb = @{ }; - $systemdbs = @("master", "msdb", "model") - - - $subdirectories = (Get-ChildItem -Directory $Path).FullName - foreach ($subdirectory in $subdirectories) - { - if ((Get-ChildItem $subdirectory).Name -eq "FULL") { $dblist += $subdirectory; continue } - } - - if ($dblist.count -eq 0) - { - throw "No databases to restore. Did you use the correct file path? Format should be \\fileshare\share\sqlbackups\sqlservername" - } - - foreach ($db in $dblist) - { - $full = Get-ChildItem "$db\FULL\*.bak" | sort LastWriteTime | select -last 1 - $since = $full.LastWriteTime; $full = $full.FullName - - $diff = $null; $logs = $null - if (Test-Path "$db\DIFF") - { - $diff = Get-ChildItem "$db\DIFF\*.bak" | Where { $_.LastWriteTime -gt $since } | sort LastWriteTime | select -last 1 - $since = $diff.LastWriteTime; $diff = $diff.fullname - } - if (Test-Path "$db\LOG") - { - $logs = (Get-ChildItem "$db\LOG\*.trn" | Where { $_.LastWriteTime -gt $since }) - $logs = ($logs | Sort-Object LastWriteTime).Fullname - } - - $restore = New-Object "Microsoft.SqlServer.Management.Smo.Restore" - $device = New-Object -TypeName Microsoft.SqlServer.Management.Smo.BackupDeviceItem $full, "FILE" - $restore.Devices.Add($device) - try { $filelist = $restore.ReadFileList($server) } - catch { throw "File list could not be determined. This is likely due to connectivity issues or tiemouts with the SQL Server, the database version is incorrect, or the SQL Server service account does not have access to the file share. Script terminating." } - - $header = $restore.ReadBackupHeader($server) - $dbname = $header.DatabaseName - - if ($systemdbs -contains $dbname) { continue } - if (!([string]::IsNullOrEmpty($Databases)) -and $Databases -notcontains $dbname) { continue } - if (!([string]::IsNullOrEmpty($Exclude)) -and $Exclude -contains $dbname) - { - $skippedb.Add($dbname, "Explicitly Skipped") - Continue - } - if ($systemdbs -contains $dbname) { continue } - - if ($server.databases[$dbname] -ne $null -and !$force -and $systemdbs -notcontains $dbname) - { - Write-Warning "$dbname exists at $SqlServer. Use -Force to drop and migrate." - $skippedb[$dbname] = "Database exists at $SqlServer. Use -Force to drop and migrate." - continue - } - - if ($server.databases[$dbname] -ne $null -and $force -and $systemdbs -notcontains $dbname) - { - If ($Pscmdlet.ShouldProcess($SqlServer, "DROP DATABASE $dbname")) - { - Write-Output "$dbname already exists. -Force was specified. Dropping $dbname on $SqlServer." - $dropresult = Remove-SqlDatabase $server $dbname - if (!$dropresult) { $skippedb[$dbname] = "Database exists and could not be dropped."; continue } - } - } - - $filestructure = Get-OfflineSqlFileStructure $server $dbname $filelist $ReuseSourceFolderStructure - - if ($filestructure -eq $false) - { - Write-Warning "$dbname contains FILESTREAM and filestreams are not supported by destination server. Skipping." - $skippedb[$dbname] = "Database contains FILESTREAM and filestreams are not supported by destination server." - continue - } - $backupinfo = $restore.ReadBackupHeader($server) - $backupversion = [version]("$($backupinfo.SoftwareVersionMajor).$($backupinfo.SoftwareVersionMinor).$($backupinfo.SoftwareVersionBuild)") - - Write-Output "Restoring FULL backup to $dbname to $SqlServer" - $result = Restore-Database $server $dbname $full "Database" $filestructure - - if ($result -eq $true) - { - if ($diff) - { - Write-Output "Restoring DIFFERENTIAL backup" - $result = Restore-Database $server $dbname $diff "Database" $filestructure - if ($result -ne $true) { $result | fl -force; return } - } - if ($logs) - { - Write-Output "Restoring $($logs.count) LOGS" - foreach ($log in $logs) - { - $result = Restore-Database $server $dbname $log "Log" $filestructure - } - } - } - - if ($result -eq $false) { Write-Warning "$dbname could not be restored."; continue } - - if ($norecovery -eq $false) - { - $sql = "RESTORE DATABASE [$dbname] WITH RECOVERY" - try - { - $server.databases['master'].ExecuteNonQuery($sql) - $migrateddb.Add($dbname, "Successfully restored.") - Write-Output "Successfully restored $dbname." - } - catch { Write-Error "$dbname could not be set to recovered." } - try - { - try - { - $sa = Get-SqlSaLogin $server - } - catch - { - $sa = "sa" - } - - $server.databases.refresh() - $server.databases[$dbname].SetOwner($sa) - $server.databases[$dbname].Alter() - Write-Output "Successfully changed $dbname dbowner to sa" - } - catch { Write-Error "Could not update dbowner to sa." } - } - - } #end of for each database folder - } - - END - { - #Clean up - $server.ConnectionContext.Disconnect() - Write-Output "Database restores complete" - } -} diff --git a/functions/Save-DbaDiagnosticQueryScript.ps1 b/functions/Save-DbaDiagnosticQueryScript.ps1 new file mode 100644 index 000000000000..a2ce6a51a949 --- /dev/null +++ b/functions/Save-DbaDiagnosticQueryScript.ps1 @@ -0,0 +1,102 @@ +function Save-DbaDiagnosticQueryScript { + <# +.SYNOPSIS +Save-DbaDiagnosticQueryScript downloads the most recent version of all Glenn Berry DMV scripts + +.DESCRIPTION +The dbatools module will have the diagnostic queries pre-installed. Use this only to update to a more recent version or specific versions. + +This function is mainly used by Invoke-DbaDiagnosticQuery, but can also be used independently to download the Glenn Berry DMV scripts. + +Use this function to pre-download the scripts from a device with an Internet connection. + +The function Invoke-DbaDiagnosticQuery will try to download these scripts automatically, but it obviously needs an internet connection to do that. + +.PARAMETER Path +Specifies the path to the output + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: André Kamman (@AndreKamman), http://clouddba.io +Tags: Diagnostic, DMV, Troubleshooting + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.EXAMPLE +Save-DbaDiagnosticQueryScript -Path c:\temp + +Downloads the most recent version of all Glenn Berry DMV scripts to the specified location. +If Path is not specified, the "My Documents" location will be used. + +#> + [CmdletBinding()] + param ( + [System.IO.FileInfo]$Path = [Environment]::GetFolderPath("mydocuments"), + [Alias('Silent')] + [switch]$EnableException + ) + function Get-WebData { + param ($uri) + try { + $data = (Invoke-WebRequest -uri $uri -ErrorAction Stop) + return $data + } + catch { + Stop-Function -Message "Invoke-WebRequest failed: $_" -Target $data -ErrorRecord $_ + return + } + } + + if (-not (Test-Path $Path)) { + Stop-Function -Message "Path does not exist or access denied" -Target $path + return + } + + Add-Type -AssemblyName System.Web + $glenberryrss = "http://www.sqlskills.com/blogs/glenn/feed/" + $glenberrysql = @() + + Write-Message -Level Output -Message "Downloading RSS Feed" + $rss = [xml](get-webdata -uri $glenberryrss) + $Feed = $rss.rss.Channel + + $glenberrysql = @() + $RssPostFilter = "SQL Server Diagnostic Information Queries for*" + $DropboxLinkFilter = "*dropbox.com*" + $LinkTitleFilter = "*Diagnostic*" + + foreach ($post in $Feed.item) { + if ($post.title -like $RssPostFilter) { + # We found the first post that matches it, lets go visit and scrape. + $page = Get-WebData -uri $post.link + $glenberrysql += ($page.Links | Where-Object { $_.href -like $DropboxLinkFilter -and $_.innerText -like $LinkTitleFilter } | ForEach-Object { + [pscustomobject]@{ + URL = $_.href + SQLVersion = $_.innerText -replace " Diagnostic Information Queries", "" -replace "SQL Server ", "" -replace ' ', '' + FileYear = ($post.title -split " ")[-1] + FileMonth = "{0:00}" -f [int]([CultureInfo]::InvariantCulture.DateTimeFormat.MonthNames.IndexOf(($post.title -split " ")[-2])) + } + }) + break + } + } + Write-Message -Level Output -Message "Found $($glenberrysql.Count) documents to download" + foreach ($doc in $glenberrysql) { + try { + Write-Message -Level Output -Message "Downloading $($doc.URL)" + $page = Invoke-WebRequest -Uri $doc.URL -ErrorAction Stop + $filename = "{0}\SQLServerDiagnosticQueries_{1}_{2}.sql" -f $Path, $doc.SQLVersion, "$($doc.FileYear)$($doc.FileMonth)" + [io.file]::WriteAllText($filename, $page.content) + } + catch { + Stop-Function -Message "Requesting and writing file failed: $_" -Target $filename -ErrorRecord $_ + return + } + } +} diff --git a/functions/Select-DbaBackupInformation.ps1 b/functions/Select-DbaBackupInformation.ps1 new file mode 100644 index 000000000000..ecddec7611ac --- /dev/null +++ b/functions/Select-DbaBackupInformation.ps1 @@ -0,0 +1,171 @@ +function Select-DbaBackupInformation { + <# + .SYNOPSIS + Select a subset of backups from a dbatools backup history object + + .DESCRIPTION + Select-DbaBackupInformation filters out a subset of backups from the dbatools backup history object with parameters supplied. + + .PARAMETER BackupHistory + A dbatools.BackupHistory object containing backup history records + + .PARAMETER RestoreTime + The point in time you want to restore to + + .PARAMETER IgnoreLogs + This switch will cause Log Backups to be ignored. So will restore to the last Full or Diff backup only + + .PARAMETER IgnoreDiffs + This switch will cause Differential backups to be ignored. Unless IgnoreLogs is specified, restore to point in time will still occur, just using all available log backups + + .PARAMETER DatabaseName + A string array of Database Names that you want to filter to + + .PARAMETER ServerName + A string array of Server Names that you want to filter + + .PARAMETER ContinuePoints + The Output of Get-RestoreContinuableDatabase while provides 'Database',redo_start_lsn,'FirstRecoveryForkID' values. Used to filter backups to continue a restore on a database + Sets IgnoreDiffs, and also filters databases to only those within the ContinuePoints object, or the ContinuePoints object AND DatabaseName if both specified + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Backup, Restore + Author:Stuart Moore (@napalmgram stuart-moore.com ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Select-DbaBackupInformation + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) + + Returns all backups needed to restore all the backups in \\server1\backups$ to 1 hour ago + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -DatabaseName ProdFinance + + Returns all the backups needed to restore Database ProdFinance to an hour ago + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -IgnoreLogs + + Returns all the backups in \\server1\backups$ to restore to as close prior to 1 hour ago as can be managed with only full and differential backups + + .EXAMPLE + $Backups = Get-DbaBackupInformation -SqlInstance Server1 -Path \\server1\backups$ + $FilteredBackups = $Backups | Select-DbaBackupInformation -RestoreTime (Get-Date).AddHours(-1) -IgnoreDiffs + + Returns all the backups in \\server1\backups$ to restore to 1 hour ago using only Full and Diff backups. + #> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object]$BackupHistory, + [DateTime]$RestoreTime = (get-date).addmonths(1), + [switch]$IgnoreLogs, + [switch]$IgnoreDiffs, + [string[]]$DatabaseName, + [string[]]$ServerName, + [object]$ContinuePoints, + [switch]$EnableException + ) + begin { + $InternalHistory = @() + + if ((Test-Bound -ParameterName ContinuePoints) -and $null -ne $ContinuePoints) { + Write-Message -Message "ContinuePoints provided so setting up for a continue" -Level Verbose + + $IgnoreDiffs = $true + $IgnoreFull = $true + if (Test-Bound -ParameterName DatabaseName) { + $DatabaseName = $DatabaseName | Where-Object {$_ -in ($ContinuePoints | Select-Object -Property Database).Database} + + $DroppedDatabases = $DatabaseName | Where-Object {$_ -notin ($ContinuePoints | Select-Object -Property Database).Database} + if ($null -ne $DroppedDatabases) { + Write-Message -Message "$($DroppedDatabases.join(',')) filtered out as not in ContinuePoints" -Level Verbose + } + } + else { + $DatabaseName = ($ContinuePoints | Select-Object -Property Database).Database + } + } + } + process { + $internalHistory += $BackupHistory + } + + end { + ForEach ($History in $InternalHistory) { + if ("RestoreTime" -notin $History.PSobject.Properties.name) { + $History | Add-Member -Name 'RestoreTime' -Type NoteProperty -Value $RestoreTime + } + } + if ((Test-Bound -ParameterName DatabaseName) -and '' -ne $DatabaseName) { + Write-Message -Message "Filtering by DatabaseName" -Level Verbose + $InternalHistory = $InternalHistory | Where-Object {$_.Database -in $DatabaseName} + } + if (Test-Bound -ParameterName ServerName) { + Write-Message -Message "Filtering by ServerName" -Level Verbose + $InternalHistory = $InternalHistory | Where-Object {$_.InstanceName -in $servername} + } + + $Databases = ($InternalHistory | Select-Object -Property Database -unique).Database + ForEach ($Database in $Databases) { + Write-Message -Message "Processing Db $Database" -Level Verbose + $DatabaseHistory = $InternalHistory | Where-Object {$_.Database -eq $Database} + + $dbHistory = @() + #Find the Last Full Backup before RestoreTime + if ($true -ne $IgnoreFull) { + $Full = $DatabaseHistory | Where-Object {$_.Type -in ('Full', 'Database') -and $_.Start -le $RestoreTime} | Sort-Object -Property LastLsn -Descending | Select-Object -First 1 + $full.Fullname = ($DatabaseHistory | Where-Object {$_.Type -in ('Full', 'Database') -and $_.BackupSetID -eq $Full.BackupSetID}).Fullname + $dbHistory += $full + } + #Find the Last diff between Full and RestoreTime + if ($true -ne $IgnoreDiffs) { + $Diff = $DatabaseHistory | Where-Object {$_.Type -in ('Differential', 'Database Differential') -and $_.Start -le $RestoreTime -and $_.DatabaseBackupLSN -eq $Full.CheckpointLSN} | Sort-Object -Property LastLsn -Descending | Select-Object -First 1 + if ($null -ne $Diff) { + $Diff.FullName = ($DatabaseHistory | Where-Object {$_.Type -in ('Differential', 'Database Differential') -and $_.BackupSetID -eq $diff.BackupSetID}).Fullname + $dbhistory += $Diff + } + } + #Get All t-logs up to restore time + if ($IgnoreFull -eq $true) { + [bigint]$LogBaseLsn = ($ContinuePoints | Where-Object {$_.Database -eq $Database}).redo_start_lsn + $FirstRecoveryForkID = ($ContinuePoints | Where-Object {$_.Database -eq $Database}).FirstRecoveryForkID + Write-Message -Message "Continuing, setting fake LastLsn - $LogBaseLSN" -Level Verbose + } + else { + Write-Message -Message "Setting LogBaseLSN" -Level Verbose + [bigint]$LogBaseLsn = ($dbHistory | Sort-Object -Property LastLsn -Descending | select-object -First 1).lastLsn.ToString() + $FirstRecoveryForkID = $Full.FirstRecoveryForkID + } + + if ($true -ne $IgnoreLogs) { + $FilteredLogs = $DatabaseHistory | Where-Object {$_.Type -in ('Log', 'Transaction Log') -and $_.Start -le $RestoreTime -and $_.LastLSN.ToString() -ge $LogBaseLsn -and $_.FirstLSN -ne $_.LastLSN} | Sort-Object -Property LastLsn, FirstLsn + $GroupedLogs = $FilteredLogs | Group-Object -Property LastLSN, FirstLSN + ForEach ($Group in $GroupedLogs) { + $Log = $DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID} | select-object -First 1 + $Log.FullName = ($DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID}).Fullname + $dbhistory += $Log + #$dbhistory += $DatabaseHistory | Where-Object {$_.BackupSetID -eq $Group.group[0].BackupSetID} + } + # Get Last T-log + $dbHistory += $DatabaseHistory | Where-Object {$_.Type -in ('Log', 'Transaction Log') -and $_.End -ge $RestoreTime -and $_.DatabaseBackupLSN -eq $Full.CheckpointLSN} | Sort-Object -Property LastLsn, FirstLsn | Select-Object -First 1 + } + + $dbHistory #| Group-Object -Property BackupSetId # -Unique + } + } +} diff --git a/functions/Set-DbaAgentAlert.ps1 b/functions/Set-DbaAgentAlert.ps1 new file mode 100644 index 000000000000..8fc1c7de33e9 --- /dev/null +++ b/functions/Set-DbaAgentAlert.ps1 @@ -0,0 +1,169 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentAlert { + <# +.SYNOPSIS +Set-DbaAgentAlert updates a the status of a SQL Agent Alert. + +.DESCRIPTION +Set-DbaAgentAlert updates an alert in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Alert +The name of the alert. + +.PARAMETER NewName +The new name for the alert. + +.PARAMETER Enabled +Enabled the alert. + +.PARAMETER Disabled +Disabled the alert. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER InputObject +Enables piping alert objects + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Garry Bargsley (@gbargsley, garrybargsley.com) +Tags: Agent, Alert + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentAlert + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error' -Disabled +Changes the alert to disabled. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error', 'Error Number 825', 'Error Number 824' -Enabled +Changes multiple alerts to enabled. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1, sql2, sql3 -Alert 'Severity 025: Fatal Error', 'Error Number 825', 'Error Number 824' -Enabled +Changes multiple alerts to enabled on multiple servers. + +.EXAMPLE +Set-DbaAgentAlert -SqlInstance sql1 -Alert 'Severity 025: Fatal Error' -Disabled -Whatif +Doesn't Change the alert but shows what would happen. + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Alert, + [string]$NewName, + [switch]$Enabled, + [switch]$Disabled, + [switch]$Force, + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Agent.Alert[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { +} + process { + + if (Test-FunctionInterrupt) { return } + + if ((-not $InputObject) -and (-not $Alert)) { + Stop-Function -Message "You must specify an alert name or pipe in results from another command" -Target $sqlinstance + return + } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + foreach ($a in $Alert) { + # Check if the alert exists + if ($server.JobServer.Alerts.Name -notcontains $a) { + Stop-Function -Message "Alert $a doesn't exists on $instance" -Target $instance + } + else { + # Get the alert + try { + $InputObject += $server.JobServer.Alerts[$a] + + # Refresh the object + $InputObject.Refresh() + } + catch { + Stop-Function -Message "Something went wrong retrieving the alert" -Target $a -ErrorRecord $_ -Continue + } + } + } + } + + foreach ($currentalert in $InputObject) { + $server = $currentalert.Parent.Parent + + #region alert options + # Settings the options for the alert + if ($NewName) { + Write-Message -Message "Setting alert name to $NewName" -Level Verbose + $currentalert.Rename($NewName) + } + + if ($Enabled) { + Write-Message -Message "Setting alert to enabled" -Level Verbose + $currentalert.IsEnabled = $true + } + + if ($Disabled) { + Write-Message -Message "Setting alert to disabled" -Level Verbose + $currentalert.IsEnabled = $false + } + + #endregion alert options + + # Execute + if ($PSCmdlet.ShouldProcess($SqlInstance, "Changing the alert $a")) { + try { + Write-Message -Message "Changing the alert" -Level Verbose + + # Change the alert + $currentalert.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the alert" -ErrorRecord $_ -Target $instance -Continue + } + Get-DbaAgentAlert -SqlInstance $server | Where-Object Name -eq $currentalert.name + } + } + } +} + { + Write-Message -Message "Finished changing alert(s)" -Level Verbose + } diff --git a/functions/Set-DbaAgentJob.ps1 b/functions/Set-DbaAgentJob.ps1 new file mode 100644 index 000000000000..a1fd75a4d788 --- /dev/null +++ b/functions/Set-DbaAgentJob.ps1 @@ -0,0 +1,512 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentJob { + <# +.SYNOPSIS +Set-DbaAgentJob updates a job. + +.DESCRIPTION +Set-DbaAgentJob updates a job in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. + +.PARAMETER Schedule +Schedule to attach to job. This can be more than one schedule. + +.PARAMETER ScheduleId +Schedule ID to attach to job. This can be more than one schedule ID. + +.PARAMETER NewName +The new name for the job. + +.PARAMETER Enabled +Enabled the job. + +.PARAMETER Disabled +Disabled the job + +.PARAMETER Description +The description of the job. + +.PARAMETER StartStepId +The identification number of the first step to execute for the job. + +.PARAMETER Category +The category of the job. + +.PARAMETER OwnerLogin +The name of the login that owns the job. + +.PARAMETER EventlogLevel +Specifies when to place an entry in the Microsoft Windows application log for this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailLevel +Specifies when to send an e-mail upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER NetsendLevel +Specifies when to send a network message upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER PageLevel +Specifies when to send a page upon the completion of this job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER EmailOperator +The e-mail name of the operator to whom the e-mail is sent when EmailLevel is reached. + +.PARAMETER NetsendOperator +The name of the operator to whom the network message is sent. + +.PARAMETER PageOperator +The name of the operator to whom a page is sent. + +.PARAMETER DeleteLevel +Specifies when to delete the job. +Allowed values 0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always" +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER InputObject +Enables piping job objects + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJob + +.EXAMPLE +Set-DbaAgentJob sql1 -Job Job1 -Disabled +Changes the job to disabled + +.EXAMPLE +Set-DbaAgentJob sql1 -Job Job1 -OwnerLogin user1 +Changes the owner of the job + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -EventLogLevel OnSuccess +Changes the job and sets the notification to write to the Windows Application event log on success + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -EmailLevel OnFailure -EmailOperator dba +Changes the job and sets the notification to send an e-mail to the e-mail operator + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1, Job2, Job3 -Enabled +Changes multiple jobs to enabled + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job Job1, Job2, Job3 -Enabled +Changes multiple jobs to enabled on multiple servers + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1 -Job Job1 -Description 'Just another job' -Whatif +Doesn't Change the job but shows what would happen. + +.EXAMPLE +Set-DbaAgentJob -SqlInstance sql1, sql2, sql3 -Job 'Job One' -Description 'Job One' +Changes a job with the name "Job1" on multiple servers to have another description + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentJob -Job Job1 -Description 'Job One' +Changes a job with the name "Job1" on multiple servers to have another description using pipe line + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Job, + [object[]]$Schedule, + [int[]]$ScheduleId, + [string]$NewName, + [switch]$Enabled, + [switch]$Disabled, + [string]$Description, + [int]$StartStepId, + [string]$Category, + [string]$OwnerLogin, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EventLogLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$EmailLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$NetsendLevel, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$PageLevel, + [string]$EmailOperator, + [string]$NetsendOperator, + [string]$PageOperator, + [ValidateSet(0, "Never", 1, "OnSuccess", 2, "OnFailure", 3, "Always")] + [object]$DeleteLevel, + [switch]$Force, + [parameter(ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Agent.Job[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + # Check of the event log level is of type string and set the integer value + if (($EventLogLevel -notin 0, 1, 2, 3) -and ($null -ne $EventLogLevel)) { + $EventLogLevel = switch ($EventLogLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the email level is of type string and set the integer value + if (($EmailLevel -notin 0, 1, 2, 3) -and ($null -ne $EmailLevel)) { + $EmailLevel = switch ($EmailLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the net send level is of type string and set the integer value + if (($NetsendLevel -notin 0, 1, 2, 3) -and ($null -ne $NetsendLevel)) { + $NetsendLevel = switch ($NetsendLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the page level is of type string and set the integer value + if (($PageLevel -notin 0, 1, 2, 3) -and ($null -ne $PageLevel)) { + $PageLevel = switch ($PageLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check of the delete level is of type string and set the integer value + if (($DeleteLevel -notin 0, 1, 2, 3) -and ($null -ne $DeleteLevel)) { + $DeleteLevel = switch ($DeleteLevel) { "Never" { 0 } "OnSuccess" { 1 } "OnFailure" { 2 } "Always" { 3 } } + } + + # Check the e-mail operator name + if (($EmailLevel -ge 1) -and (-not $EmailOperator)) { + Stop-Function -Message "Please set the e-mail operator when the e-mail level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($NetsendLevel -ge 1) -and (-not $NetsendOperator)) { + Stop-Function -Message "Please set the netsend operator when the netsend level parameter is set." -Target $sqlinstance + return + } + + # Check the e-mail operator name + if (($PageLevel -ge 1) -and (-not $PageOperator)) { + Stop-Function -Message "Please set the page operator when the page level parameter is set." -Target $sqlinstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + if ((-not $InputObject) -and (-not $Job)) { + Stop-Function -Message "You must specify a job name or pipe in results from another command" -Target $sqlinstance + return + } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($server.JobServer.Jobs.Name -notcontains $j) { + Stop-Function -Message "Job $j doesn't exists on $instance" -Target $instance + } + else { + # Get the job + try { + $InputObject += $server.JobServer.Jobs[$j] + + # Refresh the object + $InputObject.Refresh() + } + catch { + Stop-Function -Message "Something went wrong retrieving the job" -Target $j -ErrorRecord $_ -Continue + } + } + } + } + + foreach ($currentjob in $InputObject) { + $server = $currentjob.Parent.Parent + + #region job options + # Settings the options for the job + if ($NewName) { + Write-Message -Message "Setting job name to $NewName" -Level Verbose + $currentjob.Rename($NewName) + } + + if ($Schedule) { + # Loop through each of the schedules + foreach ($s in $Schedule) { + if ($server.JobServer.SharedSchedules.Name -contains $s) { + # Get the schedule ID + $sID = $server.JobServer.SharedSchedules[$s].ID + + # Add schedule to job + Write-Message -Message "Adding schedule id $sID to job" -Level Verbose + $currentjob.AddSharedSchedule($sID) + } + else { + Stop-Function -Message "Schedule $s cannot be found on instance $instance" -Target $s -Continue + } + + } + } + + if ($ScheduleId) { + # Loop through each of the schedules IDs + foreach ($sID in $ScheduleId) { + # Check if the schedule is + if ($server.JobServer.SharedSchedules.ID -contains $sID) { + # Add schedule to job + Write-Message -Message "Adding schedule id $sID to job" -Level Verbose + $currentjob.AddSharedSchedule($sID) + + } + else { + Stop-Function -Message "Schedule ID $sID cannot be found on instance $instance" -Target $sID -Continue + } + } + } + + if ($Enabled) { + Write-Message -Message "Setting job to enabled" -Level Verbose + $currentjob.IsEnabled = $true + } + + if ($Disabled) { + Write-Message -Message "Setting job to disabled" -Level Verbose + $currentjob.IsEnabled = $false + } + + if ($Description) { + Write-Message -Message "Setting job description to $Description" -Level Verbose + $currentjob.Description = $Description + } + + if ($Category) { + # Check if the job category exists + if ($Category -notin $server.JobServer.JobCategories.Name) { + if ($Force) { + if ($PSCmdlet.ShouldProcess($instance, "Creating job category on $instance")) { + try { + # Create the category + New-DbaAgentJobCategory -SqlInstance $instance -Category $Category + + Write-Message -Message "Setting job category to $Category" -Level Verbose + $currentjob.Category = $Category + } + catch { + Stop-Function -Message "Couldn't create job category $Category from $instance" -Target $instance -ErrorRecord $_ + } + } + } + else { + Stop-Function -Message "Job category $Category doesn't exist on $instance. Use -Force to create it." -Target $instance + return + } + } + else { + Write-Message -Message "Setting job category to $Category" -Level Verbose + $currentjob.Category = $Category + } + } + + if ($StartStepId) { + # Get the job steps + $currentjobSteps = $currentjob.JobSteps + + # Check if there are any job steps + if ($currentjobSteps.Count -ge 1) { + # Check if the start step id value is one of the job steps in the job + if ($currentjobSteps.ID -contains $StartStepId) { + Write-Message -Message "Setting job start step id to $StartStepId" -Level Verbose + $currentjob.StartStepID = $StartStepId + } + else { + Write-Message -Message "The step id is not present in job $j on instance $instance" -Warning + } + + } + else { + Stop-Function -Message "There are no job steps present for job $j on instance $instance" -Target $instance -Continue + } + + } + + if ($OwnerLogin) { + # Check if the login name is present on the instance + if ($server.Logins.Name -contains $OwnerLogin) { + Write-Message -Message "Setting job owner login name to $OwnerLogin" -Level Verbose + $currentjob.OwnerLoginName = $OwnerLogin + } + else { + Stop-Function -Message "The given owner log in name $OwnerLogin does not exist on instance $instance" -Target $instance -Continue + } + } + + if ($EventLogLevel) { + Write-Message -Message "Setting job event log level to $EventlogLevel" -Level Verbose + $currentjob.EventLogLevel = $EventLogLevel + } + + if ($EmailLevel) { + # Check if the notifiction needs to be removed + if ($EmailLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToEmail = $null + + # Remove the notification + $currentjob.EmailLevel = $EmailLevel + } + else { + # Check if either the operator e-mail parameter is set or the operator is set in the job + if ($EmailOperator -or $currentjob.OperatorToEmail) { + Write-Message -Message "Setting job e-mail level to $EmailLevel" -Level Verbose + $currentjob.EmailLevel = $EmailLevel + } + else { + Stop-Function -Message "Cannot set e-mail level $EmailLevel without a valid e-mail operator name" -Target $instance -Continue + } + } + } + + if ($NetsendLevel) { + # Check if the notifiction needs to be removed + if ($NetsendLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToNetSend = $null + + # Remove the notification + $currentjob.NetSendLevel = $NetsendLevel + } + else { + # Check if either the operator netsend parameter is set or the operator is set in the job + if ($NetsendOperator -or $currentjob.OperatorToNetSend) { + Write-Message -Message "Setting job netsend level to $NetsendLevel" -Level Verbose + $currentjob.NetSendLevel = $NetsendLevel + } + else { + Stop-Function -Message "Cannot set netsend level $NetsendLevel without a valid netsend operator name" -Target $instance -Continue + } + } + } + + if ($PageLevel) { + # Check if the notifiction needs to be removed + if ($PageLevel -eq 0) { + # Remove the operator + $currentjob.OperatorToPage = $null + + # Remove the notification + $currentjob.PageLevel = $PageLevel + } + else { + # Check if either the operator pager parameter is set or the operator is set in the job + if ($PageOperator -or $currentjob.OperatorToPage) { + Write-Message -Message "Setting job pager level to $PageLevel" -Level Verbose + $currentjob.PageLevel = $PageLevel + } + else { + Stop-Function -Message "Cannot set page level $PageLevel without a valid netsend operator name" -Target $instance -Continue + } + } + } + + # Check the current setting of the job's email level + if ($EmailOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $EmailOperator) { + Write-Message -Message "Setting job e-mail operator to $EmailOperator" -Level Verbose + $currentjob.OperatorToEmail = $EmailOperator + } + else { + Stop-Function -Message "The e-mail operator name $EmailOperator does not exist on instance $instance. Exiting.." -Target $j -Continue + } + } + + if ($NetsendOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $NetsendOperator) { + Write-Message -Message "Setting job netsend operator to $NetsendOperator" -Level Verbose + $currentjob.OperatorToNetSend = $NetsendOperator + } + else { + Stop-Function -Message "The netsend operator name $NetsendOperator does not exist on instance $instance. Exiting.." -Target $j -Continue + } + } + + if ($PageOperator) { + # Check if the operator name is present + if ($server.JobServer.Operators.Name -contains $PageOperator) { + Write-Message -Message "Setting job pager operator to $PageOperator" -Level Verbose + $currentjob.OperatorToPage = $PageOperator + } + else { + Stop-Function -Message "The page operator name $PageOperator does not exist on instance $instance. Exiting.." -Target $instance -Continue + } + } + + if ($DeleteLevel) { + Write-Message -Message "Setting job delete level to $DeleteLevel" -Level Verbose + $currentjob.DeleteLevel = $DeleteLevel + } + #endregion job options + + # Execute + if ($PSCmdlet.ShouldProcess($SqlInstance, "Changing the job $j")) { + try { + Write-Message -Message "Changing the job" -Level Verbose + + # Change the job + $currentjob.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the job" -ErrorRecord $_ -Target $instance -Continue + } + Get-DbaAgentJob -SqlInstance $server | Where-Object Name -eq $currentjob.name + } + } + } + + end { + Write-Message -Message "Finished changing job(s)" -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Set-DbaAgentJobCategory.ps1 b/functions/Set-DbaAgentJobCategory.ps1 new file mode 100644 index 000000000000..ffa0fd861fb7 --- /dev/null +++ b/functions/Set-DbaAgentJobCategory.ps1 @@ -0,0 +1,154 @@ +function Set-DbaAgentJobCategory { + <# +.SYNOPSIS +Set-DbaAgentJobCategory changes a job category. + +.DESCRIPTION +Set-DbaAgentJobCategory makes it possible to change a job category. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Category +The name of the category + +.PARAMETER NewName +New name of the job category + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobCategory + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJobCategory + +.EXAMPLE +New-DbaAgentJobCategory -SqlInstance sql1 -Category 'Category 1' -NewName 'Category 2' + +Change the name of the category from 'Category 1' to 'Category 2'. + +.EXAMPLE +Set-DbaAgentJobCategory -SqlInstance sql1, sql2 -Category Category1, Category2 -NewName cat1, cat2 + +Rename multiple jobs in one go on multiple servers. + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$Category, + [string[]]$NewName, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + # Create array list to hold the results + $collection = New-Object System.Collections.ArrayList + + # Check if multiple categories are being changed + if ($Category.Count -gt 1 -and $NewName.Count -eq 1) { + Stop-Function -Message "You cannot rename multiple jobs to the same name" -Target $instance + } + } + + process { + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Loop through each of the categories + foreach ($cat in $Category) { + # Check if the category exists + if ($cat -notin $server.JobServer.JobCategories.Name) { + Stop-Function -Message "Job category $cat doesn't exist on $instance" -Target $instance -Continue + } + + # Check if the category already exists + if ($NewName -and ($NewName -in $server.JobServer.JobCategories.Name)) { + Stop-Function -Message "Job category $NewName already exists on $instance" -Target $instance -Continue + } + + if ($PSCmdlet.ShouldProcess($instance, "Changing the job category $Category")) { + try { + # Get the job category object + $currentCategory = $server.JobServer.JobCategories[$cat] + + Write-Message -Message "Changing job category $cat" -Level Verbose + + # Get and set the original and new values + $originalCategoryName = $currentCategory.Name + $newCategoryName = $null + + # Check if the job category needs to be renamed + if ($NewName) { + $currentCategory.Rename($NewName[$Category.IndexOf($cat)]) + $newCategoryName = $currentCategory.Name + } + + # Set up the custom object + $null = $collection.Add([PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + CategoryName = $originalCategoryName + NewCategoryName = $newCategoryName + }) + + } + catch { + Stop-Function -Message "Something went wrong changing the job category $cat on $instance" -Target $cat -Continue -ErrorRecord $_ + } + + } # if should process + + } # for each category + + } # foreach instance + + # Return result + return $collection + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing job category." -Level Verbose + } + +} \ No newline at end of file diff --git a/functions/Set-DbaAgentJobOutputFile.ps1 b/functions/Set-DbaAgentJobOutputFile.ps1 new file mode 100644 index 000000000000..d23bd22930c4 --- /dev/null +++ b/functions/Set-DbaAgentJobOutputFile.ps1 @@ -0,0 +1,152 @@ +function Set-DbaAgentJobOutputFile { + <# + .Synopsis + Set the output file for a step within an Agent job. + + .DESCRIPTION + Sets the Output File for a step of an agent job with the Job Names and steps provided dynamically if required + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SQLCredential + Credential object used to connect to the SQL Server as a different user be it Windows or SQL Server. Windows users are determined by the existence of a backslash, so if you are intending to use an alternative Windows connection instead of a SQL login, ensure it contains a backslash. + + .PARAMETER Job + The job to process - this list is auto-populated from the server. + + .PARAMETER Step + The Agent Job Step to provide Output File Path for. Also available dynamically + + .PARAMETER OutputFile + The Full Path to the New Output file + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Agent, Job, SqlAgent + Author: Rob Sewell (https://sqldbawithabeard.com) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + # todo - allow piping and add -All + + .EXAMPLE + Set-DbaAgentJobOutputFile -SqlInstance SERVERNAME -JobName 'The Agent Job' -OutPutFile E:\Logs\AgentJobStepOutput.txt + + Sets the Job step for The Agent job on SERVERNAME to E:\Logs\AgentJobStepOutput.txt + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true, HelpMessage = 'The SQL Server Instance', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false, + Position = 0)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false, HelpMessage = 'SQL Credential', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [PSCredential]$SqlCredential, + [object[]]$Job, + [Parameter(Mandatory = $false, HelpMessage = 'The Job Step name', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [object[]]$Step, + [Parameter(Mandatory = $true, HelpMessage = 'The Full Output File Path', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ValueFromRemainingArguments = $false)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string]$OutputFile, + [Alias('Silent')] + [switch]$EnableException + ) + + foreach ($instance in $sqlinstance) { + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to: $instance" + continue + } + + if (!$Job) { + # This is because jobname isn't yet required + Write-Message -Level Warning -Message "You must specify a job using the -Job parameter." + return + } + + foreach ($name in $Job) { + $currentJob = $server.JobServer.Jobs[$name] + + if ($Step) { + $steps = $currentJob.JobSteps | Where-Object Name -in $Step + + if (!$steps) { + Write-Message -Level Warning -Message "$Step didn't return any steps" + return + } + } + else { + if (($currentJob.JobSteps).Count -gt 1) { + Write-Message -Level Output -Message "Which Job Step do you wish to add output file to?" + $steps = $currentJob.JobSteps | Out-GridView -Title "Choose the Job Steps to add an output file to" -PassThru -Verbose + } + else { + $steps = $currentJob.JobSteps + } + } + + if (!$steps) { + $steps = $currentJob.JobSteps + } + + foreach ($jobstep in $steps) { + $currentoutputfile = $jobstep.OutputFileName + + Write-Message -Level Verbose -Message "Current Output File for $currentJob is $currentoutputfile" + Write-Message -Level Verbose -Message "Adding $OutputFile to $jobstep for $currentJob" + + try { + if ($Pscmdlet.ShouldProcess($jobstep, "Changing Output File from $currentoutputfile to $OutputFile")) { + $jobstep.OutputFileName = $OutputFile + $jobstep.Alter() + $jobstep.Refresh() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Job = $currentJob.Name + JobStep = $jobstep.Name + OutputFileName = $currentoutputfile + } + } + } + catch { + Stop-Function -Message "Failed to add $OutputFile to $jobstep for $currentJob" -InnerErrorRecord $_ -Target $currentJob + } + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaAgentJobStep.ps1 b/functions/Set-DbaAgentJobStep.ps1 new file mode 100644 index 000000000000..c2430f5eb79a --- /dev/null +++ b/functions/Set-DbaAgentJobStep.ps1 @@ -0,0 +1,352 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentJobStep { + <# +.SYNOPSIS +Set-DbaAgentJobStep updates a job step. + +.DESCRIPTION +Set-DbaAgentJobStep updates a job step in the SQL Server Agent with parameters supplied. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job. Can be null if the the job id is being used. + +.PARAMETER StepName +The name of the step. + +.PARAMETER NewName +The new name for the step in case it needs to be renamed. + +.PARAMETER SubSystem +The subsystem used by the SQL Server Agent service to execute command. +Allowed values 'ActiveScripting','AnalysisCommand','AnalysisQuery','CmdExec','Distribution','LogReader','Merge','PowerShell','QueueReader','Snapshot','Ssis','TransactSql' + +.PARAMETER Command +The commands to be executed by SQLServerAgent service through subsystem. + +.PARAMETER CmdExecSuccessCode +The value returned by a CmdExec subsystem command to indicate that command executed successfully. + +.PARAMETER OnSuccessAction +The action to perform if the step succeeds. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnSuccessStepId +The ID of the step in this job to execute if the step succeeds and OnSuccessAction is "GoToNextStep". + +.PARAMETER OnFailAction +The action to perform if the step fails. +Allowed values "QuitWithSuccess" (default), "QuitWithFailure", "GoToNextStep", "GoToStep". +The text value van either be lowercase, uppercase or something in between as long as the text is correct. + +.PARAMETER OnFailStepId +The ID of the step in this job to execute if the step fails and OnFailAction is "GoToNextStep". + +.PARAMETER Database +The name of the database in which to execute a Transact-SQL step. The default is 'master'. + +.PARAMETER DatabaseUser +The name of the user account to use when executing a Transact-SQL step. The default is 'sa'. + +.PARAMETER RetryAttempts +The number of retry attempts to use if this step fails. The default is 0. + +.PARAMETER RetryInterval +The amount of time in minutes between retry attempts. The default is 0. + +.PARAMETER OutputFileName +The name of the file in which the output of this step is saved. + +.PARAMETER Flag +Sets the flag(s) for the job step. + +Flag Description +---------------------------------------------------------------------------- +AppendAllCmdExecOutputToJobHistory Job history, including command output, is appended to the job history file. +AppendToJobHistory Job history is appended to the job history file. +AppendToLogFile Job history is appended to the SQL Server log file. +AppendToTableLog Job history is appended to a log table. +LogToTableWithOverwrite Job history is written to a log table, overwriting previous contents. +None Job history is not appended to a file. +ProvideStopProcessEvent Job processing is stopped. + +.PARAMETER ProxyName +The name of the proxy that the job step runs as. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentJobStep + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -NewName Step2 +Changes the name of the step in "Job1" with the name Step1 to Step2 + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1 -Job Job1, Job2 -StepName Step1 -Database msdb +Changes job steps in multiple jobs with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1, Job2 -StepName Step1 -Database msdb +Changes job steps in multiple jobs on multiple servers with the name Step1 to msdb + +.EXAMPLE +Set-DbaAgentJobStep -SqlInstance sql1, sql2, sql3 -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb for multiple servers + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentJobStep -Job Job1 -StepName Step1 -Database msdb +Changes the database of the step in "Job1" with the name Step1 to msdb for multiple servers using pipeline + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StepName, + [Parameter(Mandatory = $false)] + [string]$NewName, + [Parameter(Mandatory = $false)] + [ValidateSet('ActiveScripting', 'AnalysisCommand', 'AnalysisQuery', 'CmdExec', 'Distribution', 'LogReader', 'Merge', 'PowerShell', 'QueueReader', 'Snapshot', 'Ssis', 'TransactSql')] + [string]$Subsystem, + [Parameter(Mandatory = $false)] + [string]$Command, + [Parameter(Mandatory = $false)] + [int]$CmdExecSuccessCode, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnSuccessAction, + [Parameter(Mandatory = $false)] + [int]$OnSuccessStepId, + [Parameter(Mandatory = $false)] + [ValidateSet('QuitWithSuccess', 'QuitWithFailure', 'GoToNextStep', 'GoToStep')] + [string]$OnFailAction, + [Parameter(Mandatory = $false)] + [int]$OnFailStepId, + [Parameter(Mandatory = $false)] + [string]$Database, + [Parameter(Mandatory = $false)] + [string]$DatabaseUser, + [Parameter(Mandatory = $false)] + [int]$RetryAttempts, + [Parameter(Mandatory = $false)] + [int]$RetryInterval, + [Parameter(Mandatory = $false)] + [string]$OutputFileName, + [Parameter(Mandatory = $false)] + [ValidateSet('AppendAllCmdExecOutputToJobHistory', 'AppendToJobHistory', 'AppendToLogFile', 'LogToTableWithOverwrite', 'None', 'ProvideStopProcessEvent')] + [string[]]$Flag, + [Parameter(Mandatory = $false)] + [string]$ProxyName, + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + begin { + # Check the parameter on success step id + if (($OnSuccessAction -ne 'GoToStep') -and ($OnSuccessStepId -ge 1)) { + Stop-Function -Message "Parameter OnSuccessStepId can only be used with OnSuccessAction 'GoToStep'." -Target $SqlInstance + return + } + + # Check the parameter on success step id + if (($OnFailAction -ne 'GoToStep') -and ($OnFailStepId -ge 1)) { + Stop-Function -Message "Parameter OnFailStepId can only be used with OnFailAction 'GoToStep'." -Target $SqlInstance + return + } + } + + process { + + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $Server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + foreach ($j in $Job) { + + # Check if the job exists + if ($Server.JobServer.Jobs.Name -notcontains $j) { + Stop-Function -Message "Job $j doesn't exists on $instance" -Target $instance -Continue + } + else { + # Check if the job step exists + if ($Server.JobServer.Jobs[$j].JobSteps.Name -notcontains $StepName) { + Stop-Function -Message "Step $StepName doesn't exists for job $j" -Target $instance -Continue + } + else { + + # Get the job step + $JobStep = $Server.JobServer.Jobs[$j].JobSteps[$StepName] + + Write-Message -Message "Modifying job $j on $instance" -Level Verbose + + #region job step options + # Setting the options for the job step + if ($NewName) { + Write-Message -Message "Setting job step name to $NewName" -Level Verbose + $JobStep.Rename($NewName) + } + + if ($Subsystem) { + Write-Message -Message "Setting job step subsystem to $Subsystem" -Level Verbose + $JobStep.Subsystem = $Subsystem + } + + if ($Command) { + Write-Message -Message "Setting job step command to $Command" -Level Verbose + $JobStep.Command = $Command + } + + if ($CmdExecSuccessCode) { + Write-Message -Message "Setting job step command exec success code to $CmdExecSuccessCode" -Level Verbose + $JobStep.CommandExecutionSuccessCode = $CmdExecSuccessCode + } + + if ($OnSuccessAction) { + Write-Message -Message "Setting job step success action to $OnSuccessAction" -Level Verbose + $JobStep.OnSuccessAction = $OnSuccessAction + } + + if ($OnSuccessStepId) { + Write-Message -Message "Setting job step success step id to $OnSuccessStepId" -Level Verbose + $JobStep.OnSuccessStep = $OnSuccessStepId + } + + if ($OnFailAction) { + Write-Message -Message "Setting job step fail action to $OnFailAction" -Level Verbose + $JobStep.OnFailAction = $OnFailAction + } + + if ($OnFailStepId) { + Write-Message -Message "Setting job step fail step id to $OnFailStepId" -Level Verbose + $JobStep.OnFailStep = $OnFailStepId + } + + if ($Database) { + # Check if the database is present on the server + if ($Server.Databases.Name -contains $Database) { + Write-Message -Message "Setting job step database name to $Database" -Level Verbose + $JobStep.DatabaseName = $Database + } + else { + Stop-Function -Message "The database is not present on instance $instance." -Target $instance -Continue + } + } + + if (($DatabaseUser) -and ($Database)) { + # Check if the username is present in the database + if ($Server.Databases[$Database].Users.Name -contains $DatabaseUser) { + Write-Message -Message "Setting job step database username to $DatabaseUser" -Level Verbose + $JobStep.DatabaseUserName = $DatabaseUser + } + else { + Stop-Function -Message "The database user is not present in the database $Database on instance $instance." -Target $instance -Continue + } + } + + if ($RetryAttempts) { + Write-Message -Message "Setting job step retry attempts to $RetryAttempts" -Level Verbose + $JobStep.RetryAttempts = $RetryAttempts + } + + if ($RetryInterval) { + Write-Message -Message "Setting job step retry interval to $RetryInterval" -Level Verbose + $JobStep.RetryInterval = $RetryInterval + } + + if ($OutputFileName) { + Write-Message -Message "Setting job step output file name to $OutputFileName" -Level Verbose + $JobStep.OutputFileName = $OutputFileName + } + + if ($ProxyName) { + # Check if the proxy exists + if ($Server.JobServer.ProxyAccounts.Name -contains $ProxyName) { + Write-Message -Message "Setting job step proxy name to $ProxyName" -Level Verbose + $JobStep.ProxyName = $ProxyName + } + else { + Stop-Function -Message "The proxy name $ProxyName doesn't exist on instance $instance." -Target $instance -Continue + } + } + + if ($Flag.Count -ge 1) { + Write-Message -Message "Setting job step flag(s) to $($Flags -join ',')" -Level Verbose + $JobStep.JobStepFlags = $Flag + } + #region job step options + + # Execute + if ($PSCmdlet.ShouldProcess($instance, "Changing the job step $StepName for job $j")) { + try { + Write-Message -Message "Changing the job step $StepName for job $j" -Level Verbose + + # Change the job step + $JobStep.Alter() + } + catch { + Stop-Function -Message "Something went wrong changing the job step" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } + + } # foreach object job + } # foreach object intance + } # process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing job step(s)" -Level Verbose + } +} \ No newline at end of file diff --git a/functions/Set-DbaAgentSchedule.ps1 b/functions/Set-DbaAgentSchedule.ps1 new file mode 100644 index 000000000000..22320386c320 --- /dev/null +++ b/functions/Set-DbaAgentSchedule.ps1 @@ -0,0 +1,447 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaAgentSchedule { + <# +.SYNOPSIS +Set-DbaAgentSchedule updates a schedule in the msdb database. + +.DESCRIPTION +Set-DbaAgentSchedule will help update a schedule for a job. It does not attach the schedule to a job. + +.PARAMETER SqlInstance +SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + +.PARAMETER SqlCredential +Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + +.PARAMETER Job +The name of the job that has the schedule. + +.PARAMETER ScheduleName +The name of the schedule. + +.PARAMETER NewName +The new name for the schedule. + +.PARAMETER Enabled +Set the schedule to enabled. + +.PARAMETER Disabled +Set the schedule to disabled. + +.PARAMETER FrequencyType +A value indicating when a job is to be executed. +Allowed values are 1, "Once", 4, "Daily", 8, "Weekly", 16, "Monthly", 32, "MonthlyRelative", 64, "AgentStart", 128 or "IdleComputer" + +.PARAMETER FrequencyInterval +The days that a job is executed +Allowed values are 1, "Sunday", 2, "Monday", 4, "Tuesday", 8, "Wednesday", 16, "Thursday", 32, "Friday", 64, "Saturday", 62, "Weekdays", 65, "Weekend", 127, "EveryDay". +If 62, "Weekdays", 65, "Weekend", 127, "EveryDay" is used it overwwrites any other value that has been passed before. + +.PARAMETER FrequencySubdayType +Specifies the units for the subday FrequencyInterval. +Allowed values are 1, "Time", 2, "Seconds", 4, "Minutes", 8 or "Hours" + +.PARAMETER FrequencySubdayInterval +The number of subday type periods to occur between each execution of a job. + +.PARAMETER FrequencySubdayInterval +The number of subday type periods to occur between each execution of a job. + +.PARAMETER FrequencyRelativeInterval +A job's occurrence of FrequencyInterval in each month, if FrequencyInterval is 32 (monthlyrelative). + +.PARAMETER FrequencyRecurrenceFactor +The number of weeks or months between the scheduled execution of a job. FrequencyRecurrenceFactor is used only if FrequencyType is 8, "Weekly", 16, "Monthly", 32 or "MonthlyRelative". + +.PARAMETER StartDate +The date on which execution of a job can begin. + +.PARAMETER EndDate +The date on which execution of a job can stop. + +.PARAMETER StartTime +The time on any day to begin execution of a job. Format HHMMSS / 24 hour clock. +Example: '010000' for 01:00:00 AM. +Example: '140000' for 02:00:00 PM. + +.PARAMETER EndTime +The time on any day to end execution of a job. Format HHMMSS / 24 hour clock. +Example: '010000' for 01:00:00 AM. +Example: '140000' for 02:00:00 PM. + +.PARAMETER Owner +The name of the server principal that owns the schedule. If no value is given the schedule is owned by the creator. + +.PARAMETER WhatIf +Shows what would happen if the command were to run. No actions are actually performed. + +.PARAMETER Confirm +Prompts you for confirmation before executing any changing operations within the command. + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + +.PARAMETER Force +The force parameter will ignore some errors in the parameters and assume defaults. +It will also remove the any present schedules with the same name for the specific job. + +.NOTES +Author: Sander Stad (@sqlstad, sqlstad.nl) +Tags: Agent, Job, JobStep + +Website: https://dbatools.io +Copyright: (C) Chrissy LeMaire, clemaire@gmail.com +License: MIT https://opensource.org/licenses/MIT + +.LINK +https://dbatools.io/Set-DbaAgentSchedule + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1 -ScheduleName daily -Enabled +Changes the schedule for Job1 with the name 'daily' to enabled + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1 -ScheduleName daily -NewName weekly -FrequencyType Weekly -FrequencyInterval Monday, Wednesday, Friday +Changes the schedule for Job1 with the name daily to have a new name weekly + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1 -Job Job1, Job2, Job3 -ScheduleName daily -StartTime '230000' +Changes the start time of the schedule for Job1 to 11 PM for multiple jobs + +.EXAMPLE +Set-DbaAgentSchedule -SqlInstance sql1, sql2, sql3 -Job Job1 -ScheduleName daily -Enabled +Changes the schedule for Job1 with the name daily to enabled on multiple servers + +.EXAMPLE +sql1, sql2, sql3 | Set-DbaAgentSchedule -Job Job1 -ScheduleName 'daily' -Enabled +Changes the schedule for Job1 with the name 'daily' to enabled on multiple servers using pipe line + +#> + + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Parameter(Mandatory = $false)] + [PSCredential]$SqlCredential, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [object[]]$Job, + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [string]$ScheduleName, + [Parameter(Mandatory = $false)] + [string]$NewName, + [Parameter(Mandatory = $false)] + [switch]$Enabled, + [Parameter(Mandatory = $false)] + [switch]$Disabled, + [ValidateSet(1, "Once", 4, "Daily", 8, "Weekly", 16, "Monthly", 32, "MonthlyRelative", 64, "AgentStart", 128, "IdleComputer")] + [object]$FrequencyType, + [Parameter(Mandatory = $false)] + [object[]]$FrequencyInterval, + [Parameter(Mandatory = $false)] + [ValidateSet(1, "Time", 2, "Seconds", 4, "Minutes", 8, "Hours")] + [object]$FrequencySubdayType, + [Parameter(Mandatory = $false)] + [int]$FrequencySubdayInterval, + [Parameter(Mandatory = $false)] + [ValidateSet('Unused', 'First', 'Second', 'Third', 'Fourth', 'Last')] + [object]$FrequencyRelativeInterval, + [Parameter(Mandatory = $false)] + [int]$FrequencyRecurrenceFactor, + [Parameter(Mandatory = $false)] + [string]$StartDate, + [Parameter(Mandatory = $false)] + [string]$EndDate, + [Parameter(Mandatory = $false)] + [string]$StartTime, + [Parameter(Mandatory = $false)] + [string]$EndTime, + [Parameter(Mandatory = $false)] + [Alias('Silent')] + [switch]$EnableException, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + begin { + + # Check of the FrequencyType value is of type string and set the integer value + if ($FrequencyType -notin 0, 1, 4, 8, 16, 32, 64, 128) { + [int]$FrequencyType = switch ($FrequencyType) { "Once" { 1 } "Daily" { 4 } "Weekly" { 8 } "Monthly" { 16 } "MonthlyRelative" { 32 } "AgentStart" { 64 } "IdleComputer" { 128 } } + } + + # Check of the FrequencySubdayType value is of type string and set the integer value + if ($FrequencySubdayType -notin 0, 1, 2, 4, 8) { + [int]$FrequencySubdayType = switch ($FrequencySubdayType) { "Time" { 1 } "Seconds" { 2 } "Minutes" { 4 } "Hours" { 8 } default {0} } + } + + # Check if the interval is valid + if (($FrequencyType -eq 4) -and ($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365)) { + Stop-Function -Message "The interval $FrequencyInterval needs to be higher than 1 and lower than 365 when using a daily frequency the interval." -Target $SqlInstance + return + } + + # Check if the recurrence factor is set for weekly or monthly interval + if (($FrequencyType -in 8, 16) -and $FrequencyRecurrenceFactor -lt 1) { + if ($Force) { + $FrequencyRecurrenceFactor = 1 + Write-Message -Message "Recurrence factor not set for weekly or monthly interval. Setting it to $FrequencyRecurrenceFactor." -Level Verbose + } + else { + Stop-Function -Message "The recurrence factor $FrequencyRecurrenceFactor needs to be at least on when using a weekly or monthly interval." -Target $SqlInstance + return + } + } + + # Check the subday interval + if (($FrequencySubdayType -in 2, 4) -and (-not ($FrequencySubdayInterval -ge 1 -or $FrequencySubdayInterval -le 59))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 59 when subday type is 2, 'Seconds', 4 or 'Minutes'" -Target $SqlInstance + return + } + elseif (($FrequencySubdayType -eq 8) -and (-not ($FrequencySubdayInterval -ge 1 -and $FrequencySubdayInterval -le 23))) { + Stop-Function -Message "Subday interval $FrequencySubdayInterval must be between 1 and 23 when subday type is 8 or 'Hours" -Target $SqlInstance + return + } + + # Check of the FrequencyInterval value is of type string and set the integer value + if (($null -ne $FrequencyType)) { + # Create the interval to hold the value(s) + [int]$Interval = 0 + + # If the FrequencyInterval is set for the weekly FrequencyType + if ($FrequencyType -eq 8) { + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 4 } + "Wednesday" { $Interval += 8 } + "Thursday" { $Interval += 16 } + "Friday" { $Interval += 32 } + "Saturday" { $Interval += 64 } + "Weekdays" { $Interval = 62 } + "Weekend" { $Interval = 65 } + "EveryDay" {$Interval = 127 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 4 { $Interval += 4 } + 8 { $Interval += 8 } + 16 { $Interval += 16 } + 31 { $Interval += 32 } + 64 { $Interval += 64 } + 62 { $Interval = 62 } + 65 { $Interval = 65 } + 127 {$Interval = 127 } + } + } + } + + # If the FrequencyInterval is set for the relative monthly FrequencyInterval + if ($FrequencyType -eq 32) { + # Loop through the array + foreach ($Item in $FrequencyInterval) { + switch ($Item) { + "Sunday" { $Interval += 1 } + "Monday" { $Interval += 2 } + "Tuesday" { $Interval += 3 } + "Wednesday" { $Interval += 4 } + "Thursday" { $Interval += 5 } + "Friday" { $Interval += 6 } + "Saturday" { $Interval += 7 } + "Day" { $Interval += 8 } + "Weekday" { $Interval += 9 } + "WeekendDay" { $Interval += 10 } + 1 { $Interval += 1 } + 2 { $Interval += 2 } + 3 { $Interval += 3 } + 4 { $Interval += 4 } + 5 { $Interval += 5 } + 6 { $Interval += 6 } + 7 { $Interval += 7 } + 8 { $Interval += 8 } + 9 { $Interval += 9 } + 10 { $Interval += 10 } + } + } + } + } + + # Check of the relative FrequencyInterval value is of type string and set the integer value + if (($FrequencyRelativeInterval -notin 1, 2, 4, 8, 16) -and $null -ne $FrequencyRelativeInterval) { + [int]$FrequencyRelativeInterval = switch ($FrequencyRelativeInterval) { "First" { 1 } "Second" { 2 } "Third" { 4 } "Fourth" { 8 } "Last" { 16 } "Unused" { 0 } default { 0 }} + } + + # Check if the interval is valid + if (($FrequencyType -eq 4) -and ($FrequencyInterval -lt 1 -or $FrequencyInterval -ge 365)) { + Stop-Function -Message "The interval $FrequencyInterval needs to be higher than 1 and lower than 365 when using a daily frequency the interval." -Target $SqlInstance + return + } + + # Setup the regex + $RegexDate = '(? + [CmdletBinding(DefaultParameterSetName = 'Credential')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Sqlcollaborative.Dbatools.Parameter.DbaCmConnectionParameter[]] + $ComputerName = $env:COMPUTERNAME, + + [Parameter(ParameterSetName = "Credential")] + [PSCredential] + $Credential, + + [Parameter(ParameterSetName = "Windows")] + [switch] + $UseWindowsCredentials, + + [switch] + $OverrideExplicitCredential, + + [switch] + $OverrideConnectionPolicy, + + [Sqlcollaborative.Dbatools.Connection.ManagementConnectionType] + $DisabledConnectionTypes = 'None', + + [switch] + $DisableBadCredentialCache, + + [switch] + $DisableCimPersistence, + + [switch] + $DisableCredentialAutoRegister, + + [switch] + $EnableCredentialFailover, + + [Parameter(ParameterSetName = "Credential")] + [switch] + $WindowsCredentialsAreBad, + + [Microsoft.Management.Infrastructure.Options.WSManSessionOptions] + $CimWinRMOptions, + + [Microsoft.Management.Infrastructure.Options.DComSessionOptions] + $CimDCOMOptions, + + [System.Management.Automation.PSCredential[]] + $AddBadCredential, + + [System.Management.Automation.PSCredential[]] + $RemoveBadCredential, + + [switch] + $ClearBadCredential, + + [switch] + $ClearCredential, + + [switch] + $ResetCredential, + + [switch] + $ResetConnectionStatus, + + [switch] + $ResetConfiguration, + + [switch] + [Alias('Silent')]$EnableException + ) + + BEGIN { + Write-Message -Level InternalComment -Message "Starting execution" + Write-Message -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" + + $disable_cache = Get-DbaConfigValue -Name 'ComputerManagement.Cache.Disable.All' -Fallback $false + } + PROCESS { + foreach ($connectionObject in $ComputerName) { + if (-not $connectionObject.Success) { Stop-Function -Message "Failed to interpret computername input: $($connectionObject.InputObject)" -Category InvalidArgument -Target $connectionObject.InputObject -Continue } + Write-Message -Level VeryVerbose -Message "Processing computer: $($connectionObject.Connection.ComputerName)" + + $connection = $connectionObject.Connection + + if ($ResetConfiguration) { + Write-Message -Level Verbose -Message "Resetting the configuration to system default" + + $connection.RestoreDefaultConfiguration() + } + + if ($ResetConnectionStatus) { + Write-Message -Level Verbose -Message "Resetting the connection status" + + $connection.CimRM = 'Unknown' + $connection.CimDCOM = 'Unknown' + $connection.Wmi = 'Unknown' + $connection.PowerShellRemoting = 'Unknown' + + $connection.LastCimRM = New-Object System.DateTime(0) + $connection.LastCimDCOM = New-Object System.DateTime(0) + $connection.LastWmi = New-Object System.DateTime(0) + $connection.LastPowerShellRemoting = New-Object System.DateTime(0) + } + + if ($ResetCredential) { + Write-Message -Level Verbose -Message "Resetting credentials" + + $connection.KnownBadCredentials.Clear() + $connection.Credentials = $null + $connection.UseWindowsCredentials = $false + $connection.WindowsCredentialsAreBad = $false + } + else { + if ($ClearBadCredential) { + Write-Message -Level Verbose -Message "Clearing bad credentials" + + $connection.KnownBadCredentials.Clear() + $connection.WindowsCredentialsAreBad = $false + } + + if ($ClearCredential) { + Write-Message -Level Verbose -Message "Clearing credentials" + + $connection.Credentials = $null + $connection.UseWindowsCredentials = $false + } + } + + foreach ($badCred in $RemoveBadCredential) { + $connection.RemoveBadCredential($badCred) + } + + foreach ($badCred in $AddBadCredential) { + $connection.AddBadCredential($badCred) + } + + if (Test-Bound "Credential") { $connection.Credentials = $Credential } + if ($UseWindowsCredentials) { + $connection.Credentials = $null + $connection.UseWindowsCredentials = $UseWindowsCredentials + } + if (Test-Bound "OverrideExplicitCredential") { $connection.OverrideExplicitCredential = $OverrideExplicitCredential } + if (Test-Bound "DisabledConnectionTypes") { $connection.DisabledConnectionTypes = $DisabledConnectionTypes } + if (Test-Bound "DisableBadCredentialCache") { $connection.DisableBadCredentialCache = $DisableBadCredentialCache } + if (Test-Bound "DisableCimPersistence") { $connection.DisableCimPersistence = $DisableCimPersistence } + if (Test-Bound "DisableCredentialAutoRegister") { $connection.DisableCredentialAutoRegister = $DisableCredentialAutoRegister } + if (Test-Bound "EnableCredentialFailover") { $connection.DisableCredentialAutoRegister = $EnableCredentialFailover } + if (Test-Bound "WindowsCredentialsAreBad") { $connection.WindowsCredentialsAreBad = $WindowsCredentialsAreBad } + if (Test-Bound "CimWinRMOptions") { $connection.CimWinRMOptions = $CimWinRMOptions } + if (Test-Bound "CimDCOMOptions") { $connection.CimDCOMOptions = $CimDCOMOptions } + if (Test-Bound "OverrideConnectionPolicy") { $connection.OverrideConnectionPolicy = $OverrideConnectionPolicy } + + if (-not $disable_cache) { + Write-Message -Level Verbose -Message "Writing connection to cache" + [Sqlcollaborative.Dbatools.Connection.ConnectionHost]::Connections[$connectionObject.Connection.ComputerName] = $connection + } + else { Write-Message -Level Verbose -Message "Skipping writing to cache, since the cache has been disabled!" } + $connection + } + } + END { + Write-Message -Level InternalComment -Message "Stopping execution" + } +} diff --git a/functions/Set-DbaConfig.ps1 b/functions/Set-DbaConfig.ps1 index b435b9386b72..73f0bb66ce9e 100644 --- a/functions/Set-DbaConfig.ps1 +++ b/functions/Set-DbaConfig.ps1 @@ -1,43 +1,61 @@ -function Set-DbaConfig -{ - <# - .SYNOPSIS - Sets configuration entries. - - .DESCRIPTION - This function creates or changes configuration values. - These are used in a larger framework to provide dynamic configuration information outside the PowerShell variable system. - - .PARAMETER Name - Name of the configuration entry. If an entry of exactly this non-casesensitive name already exists, its value will be overwritten. - Duplicate names across different modules are possible and will be treated separately. - If a name contains namespace notation and no module is set, the first namespace element will be used as module instead of name. Example: - -Name "Nordwind.Server" - Is Equivalent to - -Name "Server" -Module "Nordwind" - - .PARAMETER Value - The value to assign to the named configuration element. - - .PARAMETER Module - This allows grouping configuration elements into groups based on the module/component they server. - If this parameter is not set, the configuration element is stored under its name only, which increases the likelyhood of name conflicts in large environments. - +function Set-DbaConfig { + <# + .SYNOPSIS + Sets configuration entries. + + .DESCRIPTION + This function creates or changes configuration values. + These are used in dbatools to provide dynamic configuration information outside the PowerShell variable system. + + .PARAMETER FullName + The full name of a configuration element. Must be namespaced .. + The name can have any number of sub-segments, in order to better group configurations thematically. + + .PARAMETER Name + Name of the configuration entry. If an entry of exactly this non-casesensitive name already exists, its value will be overwritten. + Duplicate names across different modules are possible and will be treated separately. + If a name contains namespace notation and no module is set, the first namespace element will be used as module instead of name. Example: + -Name "Nordwind.Server" + Is Equivalent to + -Name "Server" -Module "Nordwind" + + .PARAMETER Module + This allows grouping configuration elements into groups based on the module/component they server. + If this parameter is not set, the configuration element is stored under its name only, which increases the likelyhood of name conflicts in large environments. + + .PARAMETER Value + The value to assign to the named configuration element. + .PARAMETER Description Using this, the configuration setting is given a description, making it easier for a user to comprehend, what a specific setting is for. - - .PARAMETER Hidden - Setting this parameter hides the configuration from casual discovery. Configurations with this set will only be returned by Get-Config, if the parameter "-Force" is used. - This should be set for all system settings a user should have no business changing (e.g. for Infrastructure related settings such as mail server). - - .PARAMETER Default - Setting this parameter causes the system to treat this configuration as a default setting. If the configuration already exists, no changes will be performed. - Useful in scenarios where for some reason it is not practical to automatically set defaults before loading userprofiles. - - .PARAMETER Silent - Replaces user friendly yellow warnings with bloody red exceptions of doom! - Use this if you want the function to throw terminating errors you want to catch. - + + .PARAMETER Validation + The name of the validation script used for input validation. + These can be used to validate make sure that input is of the proper data type. + New validation scripts can be registered using Register-PSFConfigValidation + + .PARAMETER Handler + A scriptblock that is executed when a value is being set. + Is only executed if the validation was successful (assuming there was a validation, of course) + + .PARAMETER Hidden + Setting this parameter hides the configuration from casual discovery. Configurations with this set will only be returned by Get-Config, if the parameter "-Force" is used. + This should be set for all system settings a user should have no business changing (e.g. for Infrastructure related settings such as mail server). + + .PARAMETER Default + Setting this parameter causes the system to treat this configuration as a default setting. If the configuration already exists, no changes will be performed. + Useful in scenarios where for some reason it is not practical to automatically set defaults before loading userprofiles. + + .PARAMETER Initialize + Use this when setting configurations as part of module import. + When initializing a configuration, it will only do a thing if the configuration hasn't already been initialized (So if you load the module multiple times or in multiple runspaces, it won't make a difference) + Also, if there already was a non-initialized setting set for a given configuration, it will then try to set the old value again. + This value will be processed by handlers, if any are set. + + .PARAMETER DisableValidation + This parameters disables the input validation - if any - when processing a setting. + Normally this shouldn't be circumvented, but just in case, it can be disabled. + .PARAMETER DisableHandler Internal Use Only. This parameter disables the configuration handlers. @@ -46,108 +64,217 @@ function Set-DbaConfig Sometimes it may only be used to introduce input validation. During module import, some handlers are registered and many values written to configuration. However, some of those values actually are already set as default values within the library. Processing a handler will cost a few ms. - Add up a couple dozen such events and the delay is very notable. This parameter is designed to be used during module import only, in order to speed up the import. - - .EXAMPLE - PS C:\> Set-DbaConfig -Name 'User' -Value "Friedrich" -Description "The user under which the show must go on." - - Creates a configuration entry named "User" with the value "Friedrich" - - .EXAMPLE - PS C:\> Set-DbaConfig 'ConfigLink' 'https://www.example.com/config.xml' 'Company' -Hidden - - Creates a configuration entry named "ConfigLink" in the "Company" module with the value 'https://www.example.com/config.xml'. - This entry is hidden from casual discovery using Get-Config. - - .EXAMPLE - PS C:\> Set-DbaConfig 'Network.Firewall' '10.0.0.2' -Default - - Creates a configuration entry named "Firewall" in the "Network" module with the value '10.0.0.2' - This is only set, if the setting does not exist yet. If it does, this command will apply no changes. - - .NOTES - Author: Friedrich Weinmann - Tags: Config - #> + Add up a couple dozen such events and the delay is very notable. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Config, Module + Author: Friedrich Weinmann + + .EXAMPLE + PS C:\> Set-DbaConfig -Name 'User' -Value "Friedrich" -Description "The user under which the show must go on." + + Creates a configuration entry named "User" with the value "Friedrich" + + .EXAMPLE + PS C:\> Set-DbaConfig -Name 'mymodule.User' -Value "Friedrich" -Description "The user under which the show must go on." -Handler $scriptBlock -Initialize -Validation String + + Creates a configuration entry ... + - Named "mymodule.user" + - With the value "Friedrich" + - It adds a description as noted + - It registers the scriptblock stored in $scriptBlock as handler + - It initializes the script. This block only executes the first time a it is run like this. Subsequent calls will be ignored. + - It registers the basic string input type validator + This is the default example for modules using the configuration system. + Note: While the -Handler parameter is optional, it is important to add it at the initial initialize call, if you are planning to add it. + Only then will the system validate previous settings (such as what a user might have placed in his user profile) + + .EXAMPLE + PS C:\> Set-DbaConfig 'ConfigLink' 'https://www.example.com/config.xml' 'Company' -Hidden + + Creates a configuration entry named "ConfigLink" in the "Company" module with the value 'https://www.example.com/config.xml'. + This entry is hidden from casual discovery using Get-Config. + + .EXAMPLE + PS C:\> Set-DbaConfig 'Network.Firewall' '10.0.0.2' -Default + + Creates a configuration entry named "Firewall" in the "Network" module with the value '10.0.0.2' + This is only set, if the setting does not exist yet. If it does, this command will apply no changes. + #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = "FullName")] Param ( - [Parameter(Mandatory = $true, Position = 0)] + [Parameter(ParameterSetName = "FullName", Position = 0, Mandatory = $true)] + [string] + $FullName, + + [Parameter(ParameterSetName = "Module", Position = 1, Mandatory = $true)] [string] $Name, - - [Parameter(Mandatory = $true, Position = 1)] + + [Parameter(ParameterSetName = "Module", Position = 0)] + [string] + $Module, + + [Parameter(ParameterSetName = "FullName", Position = 1)] + [Parameter(ParameterSetName = "Module", Position = 2)] [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] $Value, - - [Parameter(Position = 2)] - [string] - $Module, - + [string] $Description, - + + [string] + $Validation, + + [System.Management.Automation.ScriptBlock] + $Handler, + [switch] $Hidden, - + [switch] $Default, - + [switch] - $Silent, - + $Initialize, + [switch] - $DisableHandler + $DisableValidation, + + [switch] + $DisableHandler, + + [switch] + $EnableException ) - + #region Prepare Names - $Name = $Name.ToLower() - if ($Module) { $Module = $Module.ToLower() } - - if (-not $PSBoundParameters.ContainsKey("Module") -and ($Name -match ".+\..+")) - { - $r = $Name | select-string "^(.+?)\..+" -AllMatches - $Module = $r.Matches[0].Groups[1].Value - $Name = $Name.Substring($Module.Length + 1) + if ($PSCmdlet.ParameterSetName -eq "FullName") { + if (-not $FullName.Trim(".").Contains(".")) { + Stop-Function -Message "Invalid Name: $FullName ! At least one '.' is required, to separate module from name" -EnableException $EnableException -Category InvalidArgument + return + } + + $Module = $FullName.Split(".")[0].ToLower().Trim(".") + $Name = $FullName.Substring(($Module.Length + 1)).ToLower().Trim(".") + $internalFullName = $FullName.ToLower().Trim(".") + } + else { + $Name = $Name.ToLower().Trim(".") + if ($Module) { $Module = $Module.ToLower().Trim(".") } + + if ((Test-Bound -ParameterName "Module" -Not) -and ($Name -match ".+\..+")) { + $r = $Name | select-string "^(.+?)\..+" -AllMatches + $Module = $r.Matches[0].Groups[1].Value + $Name = $Name.Substring($Module.Length + 1) + } + elseif ((Test-Bound -ParameterName "Module" -Not) -and ($Name -notmatch ".+\..+")) { + Stop-Function -Message "Invalid Name: $Name ! At least one '.' is required when not explicitly specifying a module name, to separate module from name" -EnableException $EnableException -Category InvalidArgument + return + } + + If ($Module) { $internalFullName = $Module, $Name -join "." } + else { $internalFullName = $Name } } - - If ($Module) { $FullName = $Module, $Name -join "." } - else { $FullName = $Name } #endregion Prepare Names - - #region Process Configuration Event Handlers - if (-not $DisableHandler) - { - if ([sqlcollective.dbatools.Configuration.Config]::ConfigHandler[$FullName]) - { - $TestResult = [sqlcollective.dbatools.Configuration.Config]::ConfigHandler[$FullName].Invoke($Value) - if (-not $TestResult.Success) - { - Stop-Function -Message "Failed to process configuration: $($TestResult.Message)" -Silent $Silent -Category InvalidResult -Target $Value - return - } + + #region Prepare runtime and kill execution as needed + if ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations.ContainsKey($internalFullName)) { + $itExists = $true + $itIsInitialized = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Initialized + $itIsEnforced = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].PolicyEnforced + } + else { + $itExists = $false + $itIsInitialized = $false + $itIsEnforced = $false + } + + if ($itExists -and $Default) { return } + if ($itIsInitialized -and $Initialize) { return } + if ($itIsEnforced -and (-not $Initialize)) { + Stop-Function -Message "Could not update configuration due to policy settings: $internalFullName" -EnableException $EnableException -Category PermissionDenied + return + } + + if (Test-Bound -ParameterName "Validation") { + if (-not ([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation.Keys -contains $Validation.ToLower())) { + Stop-Function -Message "Invalid validation name: $Validation. Supported validations: $([Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation.Keys -join ", ")" -Category InvalidArgument -Target $Name + return } } - #endregion Process Configuration Event Handlers - - #region Process Record - if (([sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName]) -and (-not $Default)) - { - if ($PSBoundParameters.ContainsKey("Hidden")) { [sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName].Hidden = $Hidden } - [sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName].Value = $Value - if ($PSBoundParameters.ContainsKey("Description")) { [sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName].Description = $Description } + #endregion Prepare runtime and kill execution as needed + + #region Initializing a configuration + if ($Initialize) { + if ($itExists) { + $oldValue = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Value + $cfg = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] + } + else { $cfg = New-Object Sqlcollaborative.Dbatools.Configuration.Config } + $cfg.Name = $Name + $cfg.Module = $Module + $cfg.Description = $Description + $cfg.Value = $Value + $cfg.Handler = $Handler + if ($Validation) { $cfg.Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + $cfg.Hidden = $Hidden + $cfg.Initialized = $true + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] = $cfg + + if ($itExists) { Set-DbaConfig -Name $internalFullName -Value $oldValue } } - elseif (-not [sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName]) - { - $Config = New-Object sqlcollective.dbatools.Configuration.Config - $Config.Name = $name - $Config.Module = $Module - $Config.Description = $Description - $Config.Value = $Value - $Config.Hidden = $Hidden - [sqlcollective.dbatools.Configuration.Config]::Cfg[$FullName] = $Config + #endregion Initializing a configuration + + #region Regular configuration update + else { + if (-not $itExists) { + $cfg = New-Object Sqlcollaborative.Dbatools.Configuration.Config + $cfg.Name = $Name + $cfg.Module = $Module + $cfg.Description = $Description + $cfg.Handler = $Handler + if ($Validation) { $cfg.Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + $cfg.Hidden = $Hidden + [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] = $cfg + + Set-DbaConfig -Name $internalFullName -Value $Value + return + } + + else { + [Sqlcollaborative.Dbatools.Configuration.Config]$cfg = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName] + if ((-not $DisableValidation) -and ($cfg.Validation) -and (Test-Bound -ParameterName "Value")) { + $testResult = [scriptblock]::Create($cfg.Validation.ToString()).Invoke($Value) + if (-not $TestResult.Success) { + Stop-Function -Message "Could not update configuration $internalFullName | Failed validation: $($testResult.Message)" -EnableException $EnableException -Category InvalidResult -Target $internalFullName + return + } + $Value = $testResult.Value + } + + if (Test-Bound -ParameterName "Hidden") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Hidden = $Hidden } + if (Test-Bound -ParameterName "Value") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Value = $Value } + if (Test-Bound -ParameterName "Description") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Description = $Description } + if (Test-Bound -ParameterName "Handler") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Handler = $Handler } + if (Test-Bound -ParameterName "Validation") { [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Configurations[$internalFullName].Validation = [Sqlcollaborative.Dbatools.Configuration.ConfigurationHost]::Validation[$Validation.ToLower()] } + + if ((-not $DisableHandler) -and ($cfg.Handler) -and (Test-Bound -ParameterName "Value")) { + try { [scriptblock]::Create($cfg.Handler.ToString()).Invoke($Value) } + catch { + Stop-Function -Message "Could not update configuration $internalFullName | Failed handling $_" -EnableException $EnableException -Category InvalidResult -Target $internalFullName + return + } + } + } } - #endregion Process Record + #endregion Regular configuration update } diff --git a/functions/Set-DbaDatabaseOwner.ps1 b/functions/Set-DbaDatabaseOwner.ps1 index cb3b7d5e5277..a9626472cd93 100644 --- a/functions/Set-DbaDatabaseOwner.ps1 +++ b/functions/Set-DbaDatabaseOwner.ps1 @@ -1,181 +1,160 @@ -function Set-DbaDatabaseOwner -{ -<# -.SYNOPSIS -Sets database owners with a desired login if databases do not match that owner. - -.DESCRIPTION -This function will alter database ownershipt to match a specified login if their current owner does not match the target login. By default, the target login will be 'sa', but the fuction will allow the user to specify a different login for ownership. The user can also apply this to all databases or only to a select list of databases (passed as either a comma separated list or a string array). - -Best Practice reference: http://weblogs.sqlteam.com/dang/archive/2008/01/13/Database-Owner-Troubles.aspx - -.NOTES -Original Author: Michael Fal (@Mike_Fal), http://mikefal.net - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.PARAMETER SqlServer -SQLServer name or SMO object representing the SQL Server to connect to. This can be a collection and recieve pipeline input - -.PARAMETER SqlCredential -PSCredential object to connect under. If not specified, currend Windows login will be used. - -.PARAMETER Datbases -List of databases to apply changes to. Will accept a comma separated list or a string array. - -.PARAMETER Exclude -List of databases to exclude - -.PARAMETER TargetLogin -Specific login that you wish to check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.LINK -https://dbatools.io/Set-DbaDatabaseOwner - -.EXAMPLE -Set-DbaDatabaseOwner -SqlServer localhost - -Sets database owner to 'sa' on all databases where the owner does not match 'sa'. - -.EXAMPLE -Set-DbaDatabaseOwner -SqlServer localhost -TargetLogin DOMAIN\account - -To set the database owner to DOMAIN\account on all databases where the owner does not match DOMAIN\account. Note that TargetLogin must be a valid security principal that exists on the target server. - -.EXAMPLE -Set-DbaDatabaseOwner -SqlServer sqlserver -Databases db1, db2 - -Sets database owner to 'sa' on the db1 and db2 databases if their current owner does not match 'sa'. -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [object]$SqlCredential, - [string]$TargetLogin - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabases -SqlServer $SqlServer[0] -SqlCredential $SqlCredential } } - - BEGIN - { - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - } - - PROCESS - { - foreach ($servername in $SqlServer) - { - - #connect to the instance - Write-Verbose "Connecting to $servername" - $server = Connect-SqlServer $servername -SqlCredential $SqlCredential - - # dynamic sa name for orgs who have changed their sa name - if ($psboundparameters.TargetLogin.length -eq 0) - { - $TargetLogin = ($server.logins | Where-Object { $_.id -eq 1 }).Name - } - - #Validate login - if (($server.Logins.Name) -notcontains $TargetLogin) - { - if ($sqlserver.count -eq 1) - { - throw "Invalid login: $TargetLogin" - } - else - { - Write-Warning "$TargetLogin is not a valid login on $servername. Moving on." - Continue - } - } - - #Get database list. If value for -Databases is passed, massage to make it a string array. - #Otherwise, use all databases on the instance where owner not equal to -TargetLogin - Write-Verbose "Gathering databases to update" - +function Set-DbaDatabaseOwner { + <# + .SYNOPSIS + Sets database owners with a desired login if databases do not match that owner. + + .DESCRIPTION + This function will alter database ownership to match a specified login if their current owner does not match the target login. By default, the target login will be 'sa', but the function will allow the user to specify a different login for ownership. The user can also apply this to all databases or only to a select list of databases (passed as either a comma separated list or a string array). + + Best Practice reference: http://weblogs.sqlteam.com/dang/archive/2008/01/13/Database-Owner-Troubles.aspx + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + Specifies the database(s) to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + Specifies the database(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER TargetLogin + Specifies the login that you wish check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. This must be a valid security principal which exists on the target server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Database, Owner, DbOwner + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDatabaseOwner + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance localhost + + Sets database owner to 'sa' on all databases where the owner does not match 'sa'. + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance localhost -TargetLogin DOMAIN\account + + Sets the database owner to DOMAIN\account on all databases where the owner does not match DOMAIN\account. + + .EXAMPLE + Set-DbaDatabaseOwner -SqlInstance sqlserver -Database db1, db2 + + Sets database owner to 'sa' on the db1 and db2 databases if their current owner does not match 'sa'. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [Alias("Login")] + [string]$TargetLogin, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance." + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure." -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # dynamic sa name for orgs who have changed their sa name + if (!$TargetLogin) { + $TargetLogin = ($server.logins | Where-Object { $_.id -eq 1 }).Name + } + + #Validate login + if (($server.Logins.Name) -notcontains $TargetLogin) { + Stop-Function -Message "$TargetLogin is not a valid login on $instance. Moving on." -Continue -EnableException $EnableException + } + + #Owner cannot be a group + $TargetLoginObject = $server.Logins | where-object {$PSItem.Name -eq $TargetLogin }| Select-Object -property Name, LoginType + if ($TargetLoginObject.LoginType -eq 'WindowsGroup') { + Stop-Function -Message "$TargetLogin is a group, therefore can't be set as owner. Moving on." -Continue -EnableException $EnableException + } + + #Get database list. If value for -Database is passed, massage to make it a string array. + #Otherwise, use all databases on the instance where owner not equal to -TargetLogin #use where owner and target login do not match #exclude system dbs - $dbs = $server.Databases | Where-Object {$_.Owner -ne $TargetLogin -and @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name} + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.Owner -ne $TargetLogin -and @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name} #filter collection based on -Databases/-Exclude parameters - if ($Databases.Length -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - - if ($Exclude.Length -gt 0) - { - $dbs = $dbs | Where-Object { $Exclude -notcontains $_.Name } - } - - Write-Verbose "Updating $($dbs.Count) database(s)." - foreach ($db in $dbs) - { - $dbname = $db.name - If ($PSCmdlet.ShouldProcess($servername, "Setting database owner for $dbname to $TargetLogin")) - { - try - { - Write-Output "Setting database owner for $dbname to $TargetLogin on $servername" - # Set database owner to $TargetLogin (default 'sa') - # Ownership validations checks - - #Database is online and accessible - if($db.Status -ne 'Normal'){ - Write-Warning "$dbname on $servername is in a $($db.Status) state and can not be altered. It will be skipped." - } - #Database is updatable, not read-only - elseif ($db.IsUpdateable -eq $false) { - Write-Warning "$dbname on $servername is not in an updateable state and can not be altered. It will be skipped." - } - #Is the login mapped as a user? Logins already mapped in the database can not be the owner - elseif ($db.Users.name -contains $TargetLogin) { - Write-Warning "$dbname on $servername has $TargetLogin as a mapped user. Mapped users can not be database owners." - } - else { + if ($Database) { + $dbs = $dbs | Where-Object { $Database -contains $_.Name } + } + + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + + Write-Message -Level Verbose -Message "Updating $($dbs.Count) database(s)." + foreach ($db in $dbs) { + $dbname = $db.name + if ($PSCmdlet.ShouldProcess($instance, "Setting database owner for $dbname to $TargetLogin")) { + try { + Write-Message -Level Verbose -Message "Setting database owner for $dbname to $TargetLogin on $instance." + # Set database owner to $TargetLogin (default 'sa') + # Ownership validations checks + + #Database is online and accessible + if ($db.Status -notmatch 'Normal') { + Write-Message -Level Warning -Message "$dbname on $instance is in a $($db.Status) state and can not be altered. It will be skipped." + } + #Database is updatable, not read-only + elseif ($db.IsUpdateable -eq $false) { + Write-Message -Level Warning -Message "$dbname on $instance is not in an updateable state and can not be altered. It will be skipped." + } + #Is the login mapped as a user? Logins already mapped in the database can not be the owner + elseif ($db.Users.name -contains $TargetLogin) { + Write-Message -Level Warning -Message "$dbname on $instance has $TargetLogin as a mapped user. Mapped users can not be database owners." + } + else { $db.SetOwner($TargetLogin) + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db + Owner = $TargetLogin + } } - } - catch - { - # write-exception writes the full exception to file - Write-Exception $_ - throw $_ - Continue - } - } - } - } - } - - END - { - if ($dbs.count -eq 0) - { - Write-Output "Lookin' good! Nothing to do." - } - - Write-Verbose "Closing connection" - $server.ConnectionContext.Disconnect() - } + } + catch { + Stop-Function -Message "Failure updating owner." -ErrorRecord $_ -Target $instance -Continue + } + } + } + } + } } + diff --git a/functions/Set-DbaDatabaseState.ps1 b/functions/Set-DbaDatabaseState.ps1 index fa31fe19fd11..165d94df16db 100644 --- a/functions/Set-DbaDatabaseState.ps1 +++ b/functions/Set-DbaDatabaseState.ps1 @@ -1,552 +1,527 @@ -Function Set-DbaDatabaseState -{ -<# -.SYNOPSIS -Sets various options for databases, hereby called "states" +#ValidationTags#Messaging,FlowControl,Pipeline# +function Set-DbaDatabaseState { + <# + .SYNOPSIS + Sets various options for databases, hereby called "states" -.DESCRIPTION -Sets some common "states" on databases: - - "RW" options (ReadOnly, ReadWrite) - - "Status" options (Online, Offline, Emergency, plus a special "Detached") - - "Access" options (SingleUser, RestrictedUser, MultiUser) + .DESCRIPTION + Sets some common "states" on databases: + - "RW" options (ReadOnly, ReadWrite) + - "Status" options (Online, Offline, Emergency, plus a special "Detached") + - "Access" options (SingleUser, RestrictedUser, MultiUser) -Returns an object with SqlInstance, Database, RW, Status, Access, Notes + Returns an object with SqlInstance, Database, RW, Status, Access, Notes -Notes gets filled when something went wrong setting the state + Notes gets filled when something went wrong setting the state -.PARAMETER SqlInstance -The SQL Server that you're connecting to + .PARAMETER SqlInstance + The SQL Server that you're connecting to -.PARAMETER Credential -Credential object used to connect to the SQL Server as a different user + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server as a different user -.PARAMETER Database -Sets options only on these databases + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. if unspecified, all databases will be processed. -.PARAMETER Exclude -Sets options for all but these specific databases + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server -.PARAMETER AllDatabases -This is a parameter that was included for safety, so you don't accidentally set options on all databases without specifying + .PARAMETER AllDatabases + This is a parameter that was included for safety, so you don't accidentally set options on all databases without specifying -.PARAMETER ReadOnly -RW Option : Sets the database as READ_ONLY + .PARAMETER ReadOnly + RW Option : Sets the database as READ_ONLY -.PARAMETER ReadWrite -RW Option : Sets the database as READ_WRITE - -.PARAMETER Online -Status Option : Sets the database as ONLINE - -.PARAMETER Offline -Status Option : Sets the database as OFFLINE - -.PARAMETER Emergency -Status Option : Sets the database as EMERGENCY - -.PARAMETER Detached -Status Option : Detaches the database - -.PARAMETER SingleUser -Access Option : Sets the database as SINGLE_USER - -.PARAMETER RestrictedUser -Access Option : Sets the database as RESTRICTED_USER - -.PARAMETER MultiUser -Access Option : Sets the database as MULTI_USER - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.PARAMETER Force -For most options, this translates to istantly rolling back any open transactions -that may be stopping the process. -For -Detached it is required to break mirroring and Availability Groups - -.PARAMETER SmoDatabase -Internal parameter for piped objects - this will likely go away once we move to better dynamic parameters - -.NOTES -Author: niphlod - -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK -https://dbatools.io/Set-DbaDatabaseState - -.EXAMPLE -Set-DbaDatabaseState -SqlServer sqlserver2014a -Database HR -Offline - -Sets the HR database as OFFLINE - -.EXAMPLE -Set-DbaDatabaseState -SqlServer sqlserver2014a -AllDatabases -Exclude HR -Readonly -Force - -Sets all databases of the sqlserver2014a instance, except for HR, as READ_ONLY - -.EXAMPLE -Get-DbaDatabaseState -SqlInstance sql2016 | Where-Object Status -eq 'Offline' | Set-DbaDatabaseState -Online - -Finds all offline databases and sets them to online - -.EXAMPLE -Set-DbaDatabaseState -SqlServer sqlserver2014a -Database HR -SingleUser - -Sets the HR database as SINGLE_USER - -.EXAMPLE -Set-DbaDatabaseState -SqlServer sqlserver2014a -Database HR -SingleUser -Force - -Sets the HR database as SINGLE_USER, dropping all other connections (and rolling back open transactions) - -#> - [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Server")] - [Alias("ServerInstance", "SqlServer")] - [object[]]$SqlInstance, - [PSCredential][System.Management.Automation.CredentialAttribute()] - $Credential, - [switch]$AllDatabases, - [switch]$ReadOnly, - [switch]$ReadWrite, - [switch]$Online, - [switch]$Offline, - [switch]$Emergency, - [switch]$Detached, - [switch]$SingleUser, - [switch]$RestrictedUser, - [switch]$MultiUser, - [switch]$Force, - [parameter(Mandatory = $true, ValueFromPipeline, ParameterSetName = "Database")] - [PsCustomObject[]]$SmoDatabase - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlDatabases -SqlInstance $SqlInstance[0] -SqlCredential $Credential -NoSystem - } - } - - BEGIN - { - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - - function Get-WrongCombo($optset, $allparams) - { - $x = 0 - foreach ($opt in $optset) - { - if ($allparams.ContainsKey($opt)) { $x += 1 } - } - if ($x -gt 1) - { - $msg = $optset -Join ',-' - $msg = "You can only specify one of: -" + $msg - throw $msg - } - } - - function Edit-DatabaseState($sqlinstance, $dbname, $opt, $immediate = $false) - { - $warn = $null - $sql = "ALTER DATABASE [$dbname] SET $opt" - if ($immediate) - { - $sql += " WITH ROLLBACK IMMEDIATE" - } - else - { - $sql += " WITH NO_WAIT" - } - try - { - Write-Verbose $sql - if ($immediate) - { - # this can be helpful only for SINGLE_USER databases - # but since $immediate is called, it does no more harm - # than the immediate rollback - $sqlinstance.KillAllProcesses($dbname) - } - $null = $sqlinstance.ConnectionContext.ExecuteNonQuery($sql) - } - catch - { - Write-Exception $_ - $warn = "Failed to set '$dbname' to $opt" - Write-Warning $warn - } - return $warn - } - - $UserAccessHash = @{ - 'Single' = 'SINGLE_USER' - 'Restricted' = 'RESTRICTED_USER' - 'Multiple' = 'MULTI_USER' - } - $ReadOnlyHash = @{ - $true = 'READ_ONLY' - $false = 'READ_WRITE' - } - $StatusHash = @{ - 'Offline' = 'OFFLINE' - 'Normal' = 'ONLINE' - 'EmergencyMode' = 'EMERGENCY' - } - - function Get-DbState($db) - { - $base = [PSCustomObject]@{ - 'Access' = $null - 'Status' = $null - 'RW' = $null - } - $base.RW = $ReadOnlyHash[$db.ReadOnly] - $base.Access = $UserAccessHash[$db.UserAccess.toString()] - foreach ($status in $StatusHash.Keys) - { - if ($db.Status -match $status) - { - $base.Status = $StatusHash[$status] - break - } - } - return $base - } - - $RWExclusive = @('ReadOnly', 'ReadWrite') - $StatusExclusive = @('Online', 'Offline', 'Emergency', 'Detached') - $AccessExclusive = @('SingleUser', 'RestrictedUser', 'MultiUser') - $allparams = $PSBoundParameters - Get-WrongCombo -optset $RWExclusive -allparams $allparams - Get-WrongCombo -optset $StatusExclusive -allparams $allparams - Get-WrongCombo -optset $AccessExclusive -allparams $allparams - - $dbs = @() - } - PROCESS - { - # use PROCESS to gather info, and END to execute on it - if ($databases.Length -eq 0 -and $AllDatabases -eq $false -and !$smodatabase) - { - throw "You must specify a -AllDatabases or -Database to continue" - } - - if ($smodatabase) - { - $dbs += $smodatabase.Database - } - else - { - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $Credential - } - catch - { - Write-Warning "Can't connect to $instance" - Continue - } - $all_dbs = $server.Databases - $dbs += $all_dbs | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } - - if ($databases.count -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - if ($exclude.count -gt 0) - { - $dbs = $dbs | Where-Object { $exclude -notcontains $_.Name } - } - } - } - } - - END - { - if ($Detached -eq $true) - { - # we need to see what snaps are on the server, as base databases cannot be dropped - $snaps = $dbs | Where-Object { $_.DatabaseSnapshotBaseName.Length -gt 0 } - $snaps = $snaps | Select-Object -ExpandProperty DatabaseSnapshotBaseName | Get-Unique - } - - # need to pick up here - foreach ($db in $dbs) - { - $db_status = Get-DbState $db - - # normalizing properties returned by SMO to something more "fixed" - $warn = @() - - if ($db.DatabaseSnapshotBaseName.Length -gt 0) - { - Write-Warning "Database $db is a snapshot, skipping" - Continue - } - - if (!$Force) - { - if ($ReadOnly, $Offline, $Emergency, $SingleUser, $RestrictedUser, $Detached -contains $true) - { - if (Get-DbaProcess -SqlServer $server -SqlCredential $Credential -Databases $db.name) - { - Write-Warning "Users are currently connected to the database $db and Force was not specified. Skipping." - continue - } - } - } - - if ($ReadOnly -eq $true) - { - if ($db_status.RW -eq 'READ_ONLY') - { - Write-Verbose "Database $db is already READ_ONLY" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to READ_ONLY")) - { - Write-Verbose "Setting database $db to READ_ONLY" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_ONLY" -immediate $Force - } - } - } - - if ($ReadWrite -eq $true) - { - if ($db_status.RW -eq 'READ_WRITE') - { - Write-Verbose "Database $db is already READ_WRITE" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to READ_WRITE")) - { - Write-Verbose "Setting database $db to READ_WRITE" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_WRITE" -immediate $Force - } - } - } - - if ($Online -eq $true) - { - if ($db_status.Status -eq 'ONLINE') - { - Write-Verbose "Database $db is already ONLINE" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to ONLINE")) - { - Write-Verbose "Setting database $db to ONLINE" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "ONLINE" -immediate $Force - } - } - } - - if ($Offline -eq $true) - { - if ($db_status.Status -eq 'OFFLINE') - { - Write-Verbose "Database $db is already OFFLINE" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to OFFLINE")) - { - Write-Verbose "Setting database $db to OFFLINE" - $warn = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $Force - } - } - } - - if ($Emergency -eq $true) - { - if ($db_status.Status -eq 'EMERGENCY') - { - Write-Verbose "Database $db is already EMERGENCY" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to EMERGENCY")) - { - Write-Verbose "Setting database $db to EMERGENCY" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "EMERGENCY" -immediate $Force - } - } - } - - if ($SingleUser -eq $true) - { - if ($db_status.Access -eq 'SINGLE_USER') - { - Write-Verbose "Database $db is already SINGLE_USER" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to SINGLE_USER")) - { - Write-Verbose "Setting $db to SINGLE_USER" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "SINGLE_USER" -immediate $Force - } - } - } - - if ($RestrictedUser -eq $true) - { - if ($db_status.Access -eq 'RESTRICTED_USER') - { - Write-Verbose "Database $db is already RESTRICTED_USER" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to RESTRICTED_USER")) - { - Write-Verbose "Setting $db to RESTRICTED_USER" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "RESTRICTED_USER" -immediate $Force - } - } - } - - if ($MultiUser -eq $true) - { - if ($db_status.Access -eq 'MULTI_USER') - { - Write-Verbose "Database $db is already MULTI_USER" - } - else - { - If ($Pscmdlet.ShouldProcess($instance, "Set $db to MULTI_USER")) - { - Write-Verbose "Setting $db to MULTI_USER" - $warn += Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "MULTI_USER" -immediate $Force - } - } - } - - # Refresh info about database state here (before detaching) - $db.Refresh() - - if ($Detached -eq $true) - { - if ($db.Name -in $snaps) - { - Write-Warning "Database $db has snapshots, you need to drop them before detaching, skipping..." - Continue - } - if ($db.IsMirroringEnabled -eq $true -or $db.AvailabilityGroupName.Length -gt 0) - { - if ($Force -eq $false) - { - Write-Warning "Needs -Force to detach $db, skipping" - Continue - } - } - - if ($db.IsMirroringEnabled) - { - If ($Pscmdlet.ShouldProcess($instance, "Break mirroring for $db")) - { - try - { - $db.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) - $db.Alter() - $db.Refresh() - Write-Verbose "Broke mirroring for $db" - } - catch - { - Write-Warning "Could not break mirror for $db. Skipping." - Write-Exception $_ - Continue - } - } - } - - if ($database.AvailabilityGroupName.Length -gt 0) - { - $agname = $db.AvailabilityGroupName - If ($Pscmdlet.ShouldProcess($instance, "Removing $db from AG [$agname]")) - { - try - { - $server.AvailabilityGroups[$db.AvailabilityGroupName].AvailabilityDatabases[$db.Name].Drop() - Write-Verbose "Successfully removed $db from AG [$agname] on $server" - } - catch - { - Write-Warning "Could not remove $db from AG [$agname] on $server" - Write-Exception $_ - Continue - } - } - } - - # DBA 101 should encourage detaching just OFFLINE databases - # we can do that here - If ($Pscmdlet.ShouldProcess($instance, "Detaching $db")) - { - if ($db_status.Status -ne 'OFFLINE') - { - $opstatus = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $true - } - try - { - $sql = "EXEC master.dbo.sp_detach_db N$db" - Write-Verbose $sql - $null = $server.ConnectionContext.ExecuteNonQuery($sql) - $newstate.Status = 'DETACHED' - } - catch - { - Write-Exception $_ - Write-Warning "Failed to detach $db" - $warn += "Failed to detach" - } - } - - } - if ($warn.Count -gt 0) - { - $warn = $warn | Get-Unique - $warn = $warn -Join ';' - } - else - { - $warn = $null - } - - $db.Refresh() - $newstate = Get-DbState $db - - [PSCustomObject]@{ - SqlInstance = $server.Name - InstanceName = $server.ServiceName - ComputerName = $server.NetName - DatabaseName = $db.Name - RW = $newstate.RW - Status = $newstate.Status - Access = $newstate.Access - Notes = $warn - Database = $db - } | Select-DefaultView -ExcludeProperty Database - } - } - + .PARAMETER ReadWrite + RW Option : Sets the database as READ_WRITE + + .PARAMETER Online + Status Option : Sets the database as ONLINE + + .PARAMETER Offline + Status Option : Sets the database as OFFLINE + + .PARAMETER Emergency + Status Option : Sets the database as EMERGENCY + + .PARAMETER Detached + Status Option : Detaches the database + + .PARAMETER SingleUser + Access Option : Sets the database as SINGLE_USER + + .PARAMETER RestrictedUser + Access Option : Sets the database as RESTRICTED_USER + + .PARAMETER MultiUser + Access Option : Sets the database as MULTI_USER + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER Force + For most options, this translates to instantly rolling back any open transactions + that may be stopping the process. + For -Detached it is required to break mirroring and Availability Groups + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER InputObject + Accepts piped database objects + + .NOTES + Tags: Database, State + Author: niphlod + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDatabaseState + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -Offline + + Sets the HR database as OFFLINE + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -AllDatabases -Exclude HR -Readonly -Force + + Sets all databases of the sqlserver2014a instance, except for HR, as READ_ONLY + + .EXAMPLE + Get-DbaDatabaseState -SqlInstance sql2016 | Where-Object Status -eq 'Offline' | Set-DbaDatabaseState -Online + + Finds all offline databases and sets them to online + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -SingleUser + + Sets the HR database as SINGLE_USER + + .EXAMPLE + Set-DbaDatabaseState -SqlInstance sqlserver2014a -Database HR -SingleUser -Force + + Sets the HR database as SINGLE_USER, dropping all other connections (and rolling back open transactions) + + .EXAMPLE + Get-DbaDatabase -SqlInstance sqlserver2014a -Database HR | Set-DbaDatabaseState -SingleUser -Force + + Gets the databases from Get-DbaDatabase, and sets them as SINGLE_USER, dropping all other connections (and rolling back open transactions) + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Server")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [switch]$ReadOnly, + [switch]$ReadWrite, + [switch]$Online, + [switch]$Offline, + [switch]$Emergency, + [switch]$Detached, + [switch]$SingleUser, + [switch]$RestrictedUser, + [switch]$MultiUser, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException, + [parameter(Mandatory = $true, ValueFromPipeline, ParameterSetName = "Database")] + [PsCustomObject[]]$InputObject + ) + + begin { + function Get-WrongCombo($optset, $allparams) { + $x = 0 + foreach ($opt in $optset) { + if ($allparams.ContainsKey($opt)) { $x += 1 } + } + if ($x -gt 1) { + $msg = $optset -Join ',-' + $msg = "You can only specify one of: -" + $msg + throw $msg + } + } + + function Edit-DatabaseState($sqlinstance, $dbname, $opt, $immediate = $false) { + $warn = $null + $sql = "ALTER DATABASE [$dbname] SET $opt" + if ($immediate) { + $sql += " WITH ROLLBACK IMMEDIATE" + } + else { + $sql += " WITH NO_WAIT" + } + try { + Write-Message -Level System -Message $sql + if ($immediate) { + # this can be helpful only for SINGLE_USER databases + # but since $immediate is called, it does no more harm + # than the immediate rollback + $sqlinstance.KillAllProcesses($dbname) + } + $null = $sqlinstance.Query($sql) + } + catch { + $warn = "Failed to set '$dbname' to $opt" + Write-Message -Level Warning -Message $warn + } + return $warn + } + + $StatusHash = @{ + 'Offline' = 'OFFLINE' + 'Normal' = 'ONLINE' + 'EmergencyMode' = 'EMERGENCY' + } + + function Get-DbState($databaseName, $dbStatuses) { + $base = $dbStatuses | Where-Object DatabaseName -ceq $databaseName + foreach ($status in $StatusHash.Keys) { + if ($base.Status -match $status) { + $base.Status = $StatusHash[$status] + break + } + } + return $base + } + + $RWExclusive = @('ReadOnly', 'ReadWrite') + $StatusExclusive = @('Online', 'Offline', 'Emergency', 'Detached') + $AccessExclusive = @('SingleUser', 'RestrictedUser', 'MultiUser') + $allparams = $PSBoundParameters + try { + Get-WrongCombo -optset $RWExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + try { + Get-WrongCombo -optset $StatusExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + try { + Get-WrongCombo -optset $AccessExclusive -allparams $allparams + } + catch { + Stop-Function -Message $_ + return + } + } + process { + if (Test-FunctionInterrupt) { return } + $dbs = @() + if (!$Database -and !$AllDatabases -and !$InputObject -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify a -AllDatabases or -Database to continue" + return + } + + if ($InputObject) { + if ($InputObject.Database) { + # comes from Get-DbaDatabaseState + $dbs += $InputObject.Database + } + elseif ($InputObject.Name) { + # comes from Get-DbaDatabase + $dbs += $InputObject + } + } + else { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + $all_dbs = $server.Databases + $dbs += $all_dbs | Where-Object { @('master', 'model', 'msdb', 'tempdb', 'distribution') -notcontains $_.Name } + + if ($database) { + $dbs = $dbs | Where-Object { $database -contains $_.Name } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $ExcludeDatabase -notcontains $_.Name } + } + } + } + + # need to pick up here + foreach ($db in $dbs) { + if ($db.Name -in @('master', 'model', 'msdb', 'tempdb', 'distribution')) { + Write-Message -Level Warning -Message "Database $db is a system one, skipping" + Continue + } + $dbStatuses = @{} + $server = $db.Parent + if ($server -notin $dbStatuses.Keys) { + $dbStatuses[$server] = Get-DbaDatabaseState -SqlInstance $server + } + + # normalizing properties returned by SMO to something more "fixed" + $db_status = Get-DbState -DatabaseName $db.Name -dbStatuses $dbStatuses[$server] + + + $warn = @() + + if ($db.DatabaseSnapshotBaseName.Length -gt 0) { + Write-Message -Level Warning -Message "Database $db is a snapshot, skipping" + Continue + } + + if ($ReadOnly -eq $true) { + if ($db_status.RW -eq 'READ_ONLY') { + Write-Message -Level VeryVerbose -Message "Database $db is already READ_ONLY" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to READ_ONLY")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to READ_ONLY" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_ONLY" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.RW = 'READ_ONLY' + } + } + } + } + + if ($ReadWrite -eq $true) { + if ($db_status.RW -eq 'READ_WRITE') { + Write-Message -Level VeryVerbose -Message "Database $db is already READ_WRITE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to READ_WRITE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to READ_WRITE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "READ_WRITE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.RW = 'READ_WRITE' + } + } + } + } + + if ($Online -eq $true) { + if ($db_status.Status -eq 'ONLINE') { + Write-Message -Level VeryVerbose -Message "Database $db is already ONLINE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to ONLINE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to ONLINE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "ONLINE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.Status = 'ONLINE' + } + } + } + } + + if ($Offline -eq $true) { + if ($db_status.Status -eq 'OFFLINE') { + Write-Message -Level VeryVerbose -Message "Database $db is already OFFLINE" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to OFFLINE")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to OFFLINE" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $Force + $warn += $partial + if (!$partial) { + $db_status.Status = 'OFFLINE' + } + } + } + } + + if ($Emergency -eq $true) { + if ($db_status.Status -eq 'EMERGENCY') { + Write-Message -Level VeryVerbose -Message "Database $db is already EMERGENCY" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to EMERGENCY")) { + Write-Message -Level VeryVerbose -Message "Setting database $db to EMERGENCY" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "EMERGENCY" -immediate $Force + if (!$partial) { + $db_status.Status = 'EMERGENCY' + } + } + } + } + + if ($SingleUser -eq $true) { + if ($db_status.Access -eq 'SINGLE_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already SINGLE_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to SINGLE_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to SINGLE_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "SINGLE_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'SINGLE_USER' + } + } + } + } + + if ($RestrictedUser -eq $true) { + if ($db_status.Access -eq 'RESTRICTED_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already RESTRICTED_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to RESTRICTED_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to RESTRICTED_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "RESTRICTED_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'RESTRICTED_USER' + } + } + } + } + + if ($MultiUser -eq $true) { + if ($db_status.Access -eq 'MULTI_USER') { + Write-Message -Level VeryVerbose -Message "Database $db is already MULTI_USER" + } + else { + if ($Pscmdlet.ShouldProcess($server, "Set $db to MULTI_USER")) { + Write-Message -Level VeryVerbose -Message "Setting $db to MULTI_USER" + $partial = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "MULTI_USER" -immediate $Force + if (!$partial) { + $db_status.Access = 'MULTI_USER' + } + } + } + } + + if ($Detached -eq $true) { + # Refresh info about database state here (before detaching) + $db.Refresh() + # we need to see what snaps are on the server, as base databases cannot be dropped + $snaps = $server.Databases | Where-Object { $_.DatabaseSnapshotBaseName.Length -gt 0 } + $snaps = $snaps.DatabaseSnapshotBaseName | Get-Unique + if ($db.Name -in $snaps) { + Write-Message -Level Warning -Message "Database $db has snapshots, you need to drop them before detaching, skipping..." + Continue + } + if ($db.IsMirroringEnabled -eq $true -or $db.AvailabilityGroupName.Length -gt 0) { + if ($Force -eq $false) { + Write-Message -Level Warning -Message "Needs -Force to detach $db, skipping" + Continue + } + } + + if ($db.IsMirroringEnabled) { + if ($Pscmdlet.ShouldProcess($server, "Break mirroring for $db")) { + try { + $db.ChangeMirroringState([Microsoft.SqlServer.Management.Smo.MirroringOption]::Off) + $db.Alter() + $db.Refresh() + Write-Message -Level VeryVerbose -Message "Broke mirroring for $db" + } + catch { + Stop-Function -Message "Could not break mirror for $db. Skipping." -ErrorRecord $_ -Target $server -Continue + } + } + } + + if ($db.AvailabilityGroupName) { + $agname = $db.AvailabilityGroupName + if ($Pscmdlet.ShouldProcess($server, "Removing $db from AG [$agname]")) { + try { + $server.AvailabilityGroups[$db.AvailabilityGroupName].AvailabilityDatabases[$db.Name].Drop() + Write-Message -Level VeryVerbose -Message "Successfully removed $db from AG [$agname] on $server" + } + catch { + Stop-Function -Message "Could not remove $db from AG [$agname] on $server" -ErrorRecord $_ -Target $server -Continue + } + } + } + + # DBA 101 should encourage detaching just OFFLINE databases + # we can do that here + if ($Pscmdlet.ShouldProcess($server, "Detaching $db")) { + if ($db_status.Status -ne 'OFFLINE') { + $null = Edit-DatabaseState -sqlinstance $server -dbname $db.Name -opt "OFFLINE" -immediate $true + } + try { + $sql = "EXEC master.dbo.sp_detach_db N'$($db.Name)'" + Write-Message -Level System -Message $sql + $null = $server.Query($sql) + $db_status.Status = 'DETACHED' + } + catch { + Stop-Function -Message "Failed to detach $db" -ErrorRecord $_ -Target $server -Continue + $warn += "Failed to detach" + } + + } + + } + if ($warn) { + $warn = $warn | Get-Unique + $warn = $warn -Join ';' + } + else { + $warn = $null + } + if ($Detached -eq $true) { + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + RW = $db_status.RW + Status = $db_status.Status + Access = $db_status.Access + Notes = $warn + Database = $db + } | Select-DefaultView -ExcludeProperty Database + } + else { + $db.Refresh() + if ($null -eq $warn) { + # we avoid reenumerating properties + $newstate = $db_status + } + else { + $newstate = Get-DbState -databaseName $db.Name -dbStatuses $stateCache[$server] + } + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DatabaseName = $db.Name + RW = $newstate.RW + Status = $newstate.Status + Access = $newstate.Access + Notes = $warn + Database = $db + } | Select-DefaultView -ExcludeProperty Database + } + } + + } + + end { + + } } diff --git a/functions/Set-DbaDbCompression.ps1 b/functions/Set-DbaDbCompression.ps1 new file mode 100644 index 000000000000..94f3c1f612d6 --- /dev/null +++ b/functions/Set-DbaDbCompression.ps1 @@ -0,0 +1,323 @@ +function Set-DbaDbCompression { + <# + .SYNOPSIS + Sets tables and indexes with preferred compression setting. + + .DESCRIPTION + This function sets the appropriate compression recommendation, determined either by using the Tiger Team's query or set to the CompressionType parameter. + + Remember Uptime is critical for the Tiger Team query, the longer uptime, the more accurate the analysis is. + You would probably be best if you utilized Get-DbaUptime first, before running this command. + + Set-DbaDbCompression script derived from GitHub and the tigertoolbox + (https://github.com/Microsoft/tigertoolbox/tree/master/Evaluate-Compression-Gains) + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto populated from the server. + + .PARAMETER CompressionType + Control the compression type applied. Default is 'Recommended' which uses the Tiger Team query to use the most appropriate setting per object. Other option is to compress all objects to either Row or Page. + + .PARAMETER MaxRunTime + Will continue to alter tables and indexes for the given amount of minutes. + + .PARAMETER PercentCompression + Will only work on the tables/indexes that have the calculated savings at and higher for the given number provided. + + .PARAMETER InputObject + Takes the output of Test-DbaDbCompression as an object and applied compression based on those recommendations. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Author: Jason Squires (@js_0505, jstexasdba@gmail.com) + Tags: Compression, Table, Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDbCompression + + .EXAMPLE + Set-DbaDbCompression -SqlInstance localhost -MaxRunTime 60 -PercentCompression 25 + + Set the compression run time to 60 minutes and will start the compression of tables/indexes that have a difference of 25% or higher between current and recommended. + + .EXAMPLE + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -CompressionType Page + + Utilizes Page compression for all objects in DBName on ServerA with no time limit. + + .EXAMPLE + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -PercentCompression 25 | Out-GridView + + Will compress tables/indexes within the specified database that would show any % improvement with compression and with no time limit. The results will be piped into a nicely formated GridView. + + .EXAMPLE + $testCompression = Test-DbaDbCompression -SqlInstance ServerA -Database DBName + Set-DbaDbCompression -SqlInstance ServerA -Database DBName -InputObject $testCompression + + Gets the compression suggestions from Test-DbaDbCompression into a variable, this can then be reviewed and passed into Set-DbaDbCompression. + + .EXAMPLE + $cred = Get-Credential sqladmin + Set-DbaDbCompression -SqlInstance ServerA -ExcludeDatabase Database -SqlCredential $cred -MaxRunTime 60 -PercentCompression 25 + + Set the compression run time to 60 minutes and will start the compression of tables/indexes for all databases except the specified excluded database. Only objects that have a difference of 25% or higher between current and recommended will be compressed. + + .EXAMPLE + $servers = 'Server1','Server2' + foreach ($svr in $servers) + { + Set-DbaDbCompression -SqlInstance $svr -MaxRunTime 60 -PercentCompression 25 | Export-Csv -Path C:\temp\CompressionAnalysisPAC.csv -Append + } + + Set the compression run time to 60 minutes and will start the compression of tables/indexes across all listed servers that have a difference of 25% or higher between current and recommended. Output of command is exported to a csv. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [ValidateSet("Recommended", "Page", "Row", "None")]$CompressionType = "Recommended", + [int]$MaxRunTime = 0, + [int]$PercentCompression = 0, + $InputObject, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + $starttime = Get-Date + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $instance" -Target $instance + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 10 + } + catch { + Stop-Function -Message "Failed to process Instance $instance" -ErrorRecord $_ -Target $instance -Continue + } + + $Server.ConnectionContext.StatementTimeout = 0 + + #The reason why we do this is because of SQL 2016 and they now allow for compression on standard edition. + if ($server.EngineEdition -notmatch 'Enterprise' -and $server.VersionMajor -lt '13') { + Stop-Function -Message "Only SQL Server Enterprise Edition supports compression on $server" -Target $server -Continue + } + try { + $dbs = $server.Databases | Where-Object { $_.IsAccessible -and $_.IsSystemObject -eq 0} + if ($Database) { + $dbs = $dbs | Where-Object { $_.Name -in $Database } + } + if ($ExcludeDatabase) { + $dbs = $dbs | Where-Object { $_.Name -NotIn $ExcludeDatabase } + } + } + catch { + Stop-Function -Message "Unable to gather list of databases for $instance" -Target $instance -ErrorRecord $_ -Continue + } + + foreach ($db in $dbs) { + try { + Write-Message -Level Verbose -Message "Querying $instance - $db" + if ($db.status -ne 'Normal' -or $db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "$db is not accessible" -Target $db + continue + } + if ($db.CompatibilityLevel -lt 'Version100') { + Stop-Function -Message "$db has a compatibility level lower than Version100 and will be skipped." -Target $db -Continue + } + if ($CompressionType -eq "Recommended") { + if (Test-Bound "InputObject") { + Write-Message -Level Verbose -Message "Using passed in compression suggestions" + $compressionSuggestion = $InputObject | Where-Object {$_.Database -eq $db.name} + } + else { + Write-Message -Level Verbose -Message "Testing database for compression suggestions for $instance.$db" + $compressionSuggestion = Test-DbaDbCompression -SqlInstance $server -Database $db.Name + } + } + } + catch { + Stop-Function -Message "Unable to query $instance - $db" -Target $db -ErrorRecord $_ -Continue + } + + try { + if ($CompressionType -eq "Recommended") { + if ($Pscmdlet.ShouldProcess($db, "Applying suggested compression using results from Test-DbaDbCompression")) { + Write-Message -Level Verbose -Message "Applying suggested compression settings using Test-DbaDbCompression" + $results += $compressionSuggestion | Select-Object *, @{l = 'AlreadyProcesssed'; e = {"False"}} + foreach ($obj in ($results | Where-Object {$_.CompressionTypeRecommendation -ne 'NO_GAIN' -and $_.PercentCompression -ge $PercentCompression} | Sort-Object PercentCompression -Descending)) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + if ($obj.indexId -le 1) { + ##heaps and clustered indexes + Write-Message -Level Verbose -Message "Applying $($obj.CompressionTypeRecommendation) compression to $($obj.Database).$($obj.Schema).$($obj.TableName)" + $($server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $obj.Partition}).DataCompression = $($obj.CompressionTypeRecommendation) + $server.Databases[$obj.Database].Tables[$obj.TableName, $($obj.Schema)].Rebuild() + $obj.AlreadyProcesssed = "True" + } + else { + ##nonclustered indexes + Write-Message -Level Verbose -Message "Applying $($obj.CompressionTypeRecommendation) compression to $($obj.Database).$($obj.Schema).$($obj.TableName).$($obj.IndexName)" + $($server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].Indexes[$obj.IndexName].PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $obj.Partition}).DataCompression = $($obj.CompressionTypeRecommendation) + $server.Databases[$obj.Database].Tables[$obj.TableName, $obj.Schema].Indexes[$obj.IndexName].Rebuild() + $obj.AlreadyProcesssed = "True" + } + $obj + } + } + } + else { + if ($Pscmdlet.ShouldProcess($db, "Applying $CompressionType compression")) { + Write-Message -Level Verbose -Message "Applying $CompressionType compression to all objects in $($db.name)" + foreach ($obj in $server.Databases[$($db.name)].Tables | Where-Object {!$_.IsMemoryOptimized}) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + foreach ($p in $($obj.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing table $($obj.Schema).$($obj.Name)" + $($obj.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $obj.Rebuild() + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $null + Partition = $p.PartitionNumber + IndexID = 0 + IndexType = Switch ($obj.HasHeapIndex) {$false {"ClusteredIndex"} $true {"Heap"}} + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + + foreach ($index in $($obj.Indexes | Where-Object {!$_.IsMemoryOptimized})) { + if ($MaxRunTime -ne 0 -and ($(get-date) - $starttime).TotalMinutes -ge $MaxRunTime) { + Write-Message -Level Verbose -Message "Reached max run time of $MaxRunTime" + break + } + foreach ($p in $($index.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing $($Index.IndexType) $($Index.Name) Partition $($p.PartitionNumber)" + + ## There is a bug in SMO where setting compression to None at the index level doesn't work + ## Once this UserVoice item is fixed the workaround can be removed + ## https://feedback.azure.com/forums/908035-sql-server/suggestions/34080112-data-compression-smo-bug + if ($CompressionType -eq "None") { + $query = "ALTER INDEX [$($index.Name)] ON $($index.Parent) REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = $CompressionType)" + $Server.Query($query, $db.Name) + } + else { + $($Index.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $index.Rebuild() + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $index.Name + Partition = $p.PartitionNumber + IndexID = $index.Id + IndexType = $index.IndexType + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + } + } + foreach ($index in $($server.Databases[$($db.name)].Views | Where-Object {$_.Indexes}).Indexes) { + foreach ($p in $($index.PhysicalPartitions | Where-Object {$_.DataCompression -ne $CompressionType})) { + Write-Message -Level Verbose -Message "Compressing $($index.IndexType) $($index.Name) Partition $($p.PartitionNumber)" + + ## There is a bug in SMO where setting compression to None at the index level doesn't work + ## Once this UserVoice item is fixed the workaround can be removed + ## https://feedback.azure.com/forums/908035-sql-server/suggestions/34080112-data-compression-smo-bug + if ($CompressionType -eq "None") { + $query = "ALTER INDEX [$($index.Name)] ON $($index.Parent) REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = $CompressionType)" + $query + $Server.Query($query, $db.Name) + } + else { + $($index.PhysicalPartitions | Where-Object {$_.PartitionNumber -eq $P.PartitionNumber}).DataCompression = $CompressionType + $index.Rebuild() + } + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Database = $db.Name + Schema = $obj.Schema + TableName = $obj.Name + IndexName = $index.Name + Partition = $p.PartitionNumber + IndexID = $index.Id + IndexType = $index.IndexType + PercentScan = $null + PercentUpdate = $null + RowEstimatePercentOriginal = $null + PageEstimatePercentOriginal = $null + CompressionTypeRecommendation = $CompressionType.ToUpper() + SizeCurrent = $null + SizeRequested = $null + PercentCompression = $null + AlreadyProcesssed = "True" + } + } + } + } + } + } + catch { + Stop-Function -Message "Compression failed for $instance - $db" -Target $db -ErrorRecord $_ -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaDbQueryStoreOptions.ps1 b/functions/Set-DbaDbQueryStoreOptions.ps1 new file mode 100644 index 000000000000..92267476d84c --- /dev/null +++ b/functions/Set-DbaDbQueryStoreOptions.ps1 @@ -0,0 +1,222 @@ +#ValidationTags#CodeStyle,Messaging,FlowControl,Pipeline# +function Set-DbaDbQueryStoreOptions { + <# + .SYNOPSIS + Configure Query Store settings for a specific or multiple databases. + + .DESCRIPTION + Configure Query Store settings for a specific or multiple databases. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + SqlCredential object used to connect to the SQL Server as a different user. + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. If unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllDatabases + Run command against all user databases + + .PARAMETER State + Set the state of the Query Store. Valid options are "ReadWrite", "ReadOnly" and "Off". + + .PARAMETER FlushInterval + Set the flush to disk interval of the Query Store in seconds. + + .PARAMETER CollectionInterval + Set the runtime statistics collection interval of the Query Store in minutes. + + .PARAMETER MaxSize + Set the maximum size of the Query Store in MB. + + .PARAMETER CaptureMode + Set the query capture mode of the Query Store. Valid options are "Auto" and "All". + + .PARAMETER CleanupMode + Set the query cleanup mode policy. Valid options are "Auto" and "Off". + + .PARAMETER StaleQueryThreshold + Set the stale query threshold in days. + + .PARAMETER WhatIf + Shows what would happen if the command were to run + + .PARAMETER Confirm + Prompts for confirmation of every step. For example: + + Are you sure you want to perform this action? + Performing the operation "Changing Desired State" on target "pubs on SQL2016\VNEXT". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: QueryStore + Author: Enrico van de Laar ( @evdlaar ) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaQueryStoreOptions + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode All -CleanupMode Auto -StaleQueryThreshold 100 -AllDatabases + + Configure the Query Store settings for all user databases in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -FlushInterval 600 + + Only configure the FlushInterval setting for all Query Store databases in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -Database AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 + + Configure the Query Store settings for the AdventureWorks database in the ServerA\SQL Instance. + + .EXAMPLE + Set-DbaDbQueryStoreOptions -SqlInstance ServerA\SQL -Exclude AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 + + Configure the Query Store settings for all user databases except the AdventureWorks database in the ServerA\SQL Instance. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential] + $SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [ValidateSet('ReadWrite', 'ReadOnly', 'Off')] + [string[]]$State, + [int64]$FlushInterval, + [int64]$CollectionInterval, + [int64]$MaxSize, + [ValidateSet('Auto', 'All')] + [string[]]$CaptureMode, + [ValidateSet('Auto', 'Off')] + [string[]]$CleanupMode, + [int64]$StaleQueryThreshold, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $ExcludeDatabase += 'master', 'tempdb' + } + + process { + if (!$Database -and !$ExcludeDatabase -and !$AllDatabases) { + Stop-Function -Message "You must specify a database(s) to execute against using either -Database, -ExcludeDatabase or -AllDatabases" + return + } + + if (!$State -and !$FlushInterval -and !$CollectionInterval -and !$MaxSize -and !$CaptureMode -and !$CleanupMode -and !$StaleQueryThreshold) { + Stop-Function -Message "You must specify something to change." + return + } + + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 13 + + } + catch { + Stop-Function -Message "Can't connect to $instance. Moving on." -Category InvalidOperation -InnerErrorRecord $_ -Target $instance -Continue + } + + # We have to exclude all the system databases since they cannot have the Query Store feature enabled + $dbs = Get-DbaDatabase -SqlInstance $server -ExcludeDatabase $ExcludeDatabase -Database $Database | Where-Object IsAccessible + + foreach ($db in $dbs) { + Write-Message -Level Verbose -Message "Processing $($db.name) on $instance" + + if ($db.IsAccessible -eq $false) { + Write-Message -Level Warning -Message "The database $db on server $instance is not accessible. Skipping database." + continue + } + + if ($State) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DesiredState to $state")) { + $db.QueryStoreOptions.DesiredState = $State + $db.QueryStoreOptions.Alter() + $db.QueryStoreOptions.Refresh() + } + } + + if ($db.QueryStoreOptions.DesiredState -eq "Off" -and (Test-Bound -Parameter State -Not)) { + Write-Message -Level Warning -Message "State is set to Off; cannot change values. Please update State to ReadOnly or ReadWrite." + continue + } + + if ($FlushInterval) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DataFlushIntervalInSeconds to $FlushInterval")) { + $db.QueryStoreOptions.DataFlushIntervalInSeconds = $FlushInterval + } + } + + if ($CollectionInterval) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StatisticsCollectionIntervalInMinutes to $CollectionInterval")) { + $db.QueryStoreOptions.StatisticsCollectionIntervalInMinutes = $CollectionInterval + } + } + + if ($MaxSize) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing MaxStorageSizeInMB to $MaxSize")) { + $db.QueryStoreOptions.MaxStorageSizeInMB = $MaxSize + } + } + + if ($CaptureMode) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing QueryCaptureMode to $CaptureMode")) { + $db.QueryStoreOptions.QueryCaptureMode = $CaptureMode + } + } + + if ($CleanupMode) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing SizeBasedCleanupMode to $CleanupMode")) { + $db.QueryStoreOptions.SizeBasedCleanupMode = $CleanupMode + } + } + + if ($StaleQueryThreshold) { + if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StaleQueryThresholdInDays to $StaleQueryThreshold")) { + $db.QueryStoreOptions.StaleQueryThresholdInDays = $StaleQueryThreshold + } + } + + # Alter the Query Store Configuration + if ($Pscmdlet.ShouldProcess("$db on $instance", "Altering Query Store configuration on database")) { + try { + $db.QueryStoreOptions.Alter() + $db.Alter() + $db.Refresh() + } + catch { + Stop-Function -Message "Could not modify configuration." -Category InvalidOperation -InnerErrorRecord $_ -Target $db -Continue + } + } + + if ($Pscmdlet.ShouldProcess("$db on $instance", "Getting results from Get-DbaDbQueryStoreOptions")) { + # Display resulting changes + Get-DbaDbQueryStoreOptions -SqlInstance $server -Database $db.name -Verbose:$false + } + } + } + } +} + diff --git a/functions/Set-DbaDbRecoveryModel.ps1 b/functions/Set-DbaDbRecoveryModel.ps1 new file mode 100644 index 000000000000..f98215e2adf0 --- /dev/null +++ b/functions/Set-DbaDbRecoveryModel.ps1 @@ -0,0 +1,150 @@ +function Set-DbaDbRecoveryModel { + <# + .SYNOPSIS + Set-DbaDbRecoveryModel sets the Recovery Model. + + .DESCRIPTION + Set-DbaDbRecoveryModel sets the Recovery Model for user databases. + + .PARAMETER SqlInstance + The target SQL Server instance or instances. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Database + The database(s) to process - this list is auto-populated from the server. if unspecified, all databases will be processed. + + .PARAMETER ExcludeDatabase + The database(s) to exclude - this list is auto-populated from the server + + .PARAMETER AllDatabases + This is a parameter that was included for safety, so you don't accidentally set options on all databases without specifying + + .PARAMETER RecoveryModel + Recovery Model to be set. Valid options are 'Simple', 'Full', 'BulkLogged' + + Details about the recovery models can be found here: + https://docs.microsoft.com/en-us/sql/relational-databases/backup-restore/recovery-models-sql-server + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + Prompts for confirmation. For example: + + Are you sure you want to perform this action? + Performing the operation "ALTER DATABASE [model] SET RECOVERY Full" on target "[model] on WERES14224". + [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER InputObject + A collection of databases (such as returned by Get-DbaDatabase) + + .NOTES + Tags: Recovery, RecoveryModel, Simple, Full, Bulk, BulkLogged + Author: Viorel Ciucu (@viorelciucu), https://www.cviorel.com + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaDbRecoveryModel + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel BulkLogged -Database model -Confirm:$true -Verbose + + Sets the Recovery Model to BulkLogged for database [model] on SQL Server instance sql2014. User is requested to confirm the action. + + .EXAMPLE + Get-DbaDatabase -SqlInstance sql2014 -Database TestDB | Set-DbaDbRecoveryModel -RecoveryModel Simple -Confirm:$false + + Sets the Recovery Model to Simple for database [TestDB] on SQL Server instance sql2014. Confirmation is not required. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel Simple -Database TestDB -Confirm:$false + + Sets the Recovery Model to Simple for database [TestDB] on SQL Server instance sql2014. Confirmation is not required. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel Simple -AllDatabases -Confirm:$false + + Sets the Recovery Model to Simple for ALL uses databases MODEL database on SQL Server instance sql2014. Runs without asking for confirmation. + + .EXAMPLE + Set-DbaDbRecoveryModel -SqlInstance sql2014 -RecoveryModel BulkLogged -Database TestDB1, TestDB2 -Confirm:$false -Verbose + + Sets the Recovery Model to BulkLogged for [TestDB1] and [TestDB2] databases on SQL Server instance sql2014. Runs without asking for confirmation. + #> + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess, ConfirmImpact = 'High')] + param ( + [parameter(Mandatory, ParameterSetName = "Instance")] + [Alias("ServerInstance", "SqlServer")] + [DbaInstance[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory)] + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [string]$RecoveryModel, + [object[]]$Database, + [object[]]$ExcludeDatabase, + [switch]$AllDatabases, + [switch]$EnableException, + [parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Pipeline")] + [Microsoft.SqlServer.Management.Smo.Database[]]$InputObject + ) + process { + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + if (!$Database -and !$AllDatabases -and !$ExcludeDatabase) { + Stop-Function -Message "You must specify -AllDatabases or -Database to continue" + return + } + + # We need to be able to change the RecoveryModel for model database + $systemdbs = @("tempdb") + $databases = $server.Databases | Where-Object { $systemdbs -notcontains $_.Name -and $_.IsAccessible } + + # filter collection based on -Database/-Exclude parameters + if ($Database) { + $databases = $databases | Where-Object Name -In $Database + } + + if ($ExcludeDatabase) { + $databases = $databases | Where-Object Name -NotIn $ExcludeDatabase + } + + if (!$databases) { + Stop-Function -Message "The database(s) you specified do not exist on the instance $instance." + return + } + + $InputObject += $databases + } + + foreach ($db in $InputObject) { + if ($db.RecoveryModel -eq $RecoveryModel) { + Stop-Function -Message "Recovery Model for database $db is already set to $RecoveryModel" -Category ConnectionError -Target $instance -Continue + } + else { + $db.RecoveryModel = $RecoveryModel; + if ($Pscmdlet.ShouldProcess("$db on $instance", "ALTER DATABASE $db SET RECOVERY $RecoveryModel")) { + $db.Alter() + Write-Message -Level Verbose -Message "Recovery Model set to $RecoveryModel for database $db" + } + } + Get-DbaDbRecoveryModel -SqlInstance $db.Parent -Database $db.name + } + } +} diff --git a/functions/Set-DbaErrorLog.ps1 b/functions/Set-DbaErrorLog.ps1 new file mode 100644 index 000000000000..59e0c671e210 --- /dev/null +++ b/functions/Set-DbaErrorLog.ps1 @@ -0,0 +1,164 @@ +function Set-DbaErrorLog { + <# + .SYNOPSIS + Set the configuration for the ErrorLog on a given SQL Server instance + .DESCRIPTION + Set the configuration for the ErrorLog on a given SQL Server instance. + Includes setting the number of log files configured and/or size in KB (SQL Server 2012+ only) + + .PARAMETER SqlInstance + The target SQL Server instance(s) + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER NumberOfLog + Integer value between 6 and 99 for setting the number of error log files to keep for SQL Server instance. + + .PARAMETER SizeInKb + Integer value for the size in KB that you want the error log file to grow. This is feature only in SQL Server 2012 and higher. When the file reaches that limit SQL Server will roll the error log over. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Instance, ErrorLog + Author: Shawn Melton (@wsmelton) + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: GNU GPL v3 https://opensource.org/licenses/GPL-3.0 + + .LINK + https://dbatools.io/Set-DbaErrorLog + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2017,server2014 -NumberOfLog 25 + + Sets the number of error log files to 25 on server2017 and server2014 + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2014 -SizeInKb 1024 + + Sets the size of the error log file, before it rolls over, to 1024KB (1GB) on server2014 + + .EXAMPLE + Set-DbaErrorLog -SqlInstance server2012 -NumberOfLog 25 -SizeInKb 500 + + Sets the number of error log files to 25 and size before it will roll over to 500KB on server2012 + #> + [cmdletbinding(SupportsShouldProcess)] + param( + [Parameter(ValueFromPipelineByPropertyName)] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [DbaValidatePattern('^[6-9][0-9]?$', ErrorMessage = "Error processing {0} - input must be an integer between 6 and 99")] + [int]$NumberOfLog, + [int]$SizeInKb, + [switch]$EnableException + ) + process { + foreach ($instance in $SqlInstance) { + Write-Message -Level Verbose -Message "Connecting to $instance" + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $collectionNumberLog = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Setting = 'NumberOfLogFiles' + OriginalNumberErrorLogs = $null + CurrentNumberErrorLogs = $null + Status = $null + } + $collectionErrorLogSize = [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + Setting = 'ErrorLogSizeKb' + OriginalErrorLogSizeKb = $null + CurrentErrorLogSizeKb = $null + Status = $null + } + if (Test-Bound 'NumberOfLog') { + try { + $currentNumLogs = $server.NumberOfLogFiles + $collectionNumberLog.OriginalNumberErrorLogs = $currentNumLogs + } + catch { + $collectionNumberLog.Status = "Failed collection" + $collectionNumberLog + Stop-Function -Message "Issue collecting current value for number of error logs" -Target $server -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + + if ($NumberOfLog -eq $currentNumLogs) { + Write-Message -Level Warning -Message "The provided value for NumberOfLog is already set" -Continue + } + else { + if ($PSCmdlet.ShouldProcess($server, "Setting number of logs from [$currentNumLogs] to [$NumberOfLog]")) { + try { + $server.NumberOfLogFiles = $NumberOfLog + $server.Alter() + } + catch { + $collectionNumberLog.Status = "Failed update" + $collectionNumberLog + Stop-Function -Message "Issue setting number of log files" -Target $instance -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + if ($PSCmdlet.ShouldProcess($server, "Output final results of setting number of log files")) { + $server.Refresh() + $collectionNumberLog.CurrentNumberErrorLogs = $server.NumberOfLogFiles + $collectionNumberLog + } + } + } + if (Test-Bound 'SizeInKb') { + try { + $currentSizeInKb = $server.ErrorInSizeKb + $collectionErrorLogSize.OriginalErrorLogSizeKb = $currentSizeInKb + } + catch { + $collectionErrorLogSize.Status = "Failed collection" + $collectionErrorLogSize + Stop-Function -Message "Issue collecting current value for number of error logs" -Target $server -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + + if ($SizeInKb -eq $currentSizeInKb) { + Write-Message -Level Warning -Message "The provided value for SizeInKb is already set" -Continue + } + else { + if ($PSCmdlet.ShouldProcess($server, "Setting number of logs from [$currentSizeInKb] to [$SizeInKb]")) { + try { + $server.ErrorLogSizeKb = $SizeInKb + $server.Alter() + } + catch { + $collectionErrorLogSize.Status = "Failed update" + $collectionErrorLogSize + Stop-Function -Message "Issue setting number of log files" -Target $instance -ErrorRecord $_ -Exception $_.Exception.InnerException.InnerException.InnerException -Continue + } + } + if ($PSCmdlet.ShouldProcess($server, "Output final results of setting error log size")) { + $server.Refresh() + $collectionErrorLogSize.CurrentErrorLogSizeKb = $server.ErrorLogSizeKb + $collectionErrorLogSize + } + } + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaJobOwner.ps1 b/functions/Set-DbaJobOwner.ps1 index 387dc45c855b..b3989b1ca181 100644 --- a/functions/Set-DbaJobOwner.ps1 +++ b/functions/Set-DbaJobOwner.ps1 @@ -1,181 +1,143 @@ -function Set-DbaJobOwner -{ -<# -.SYNOPSIS -Sets SQL Agent job owners with a desired login if jobs do not match that owner. - -.DESCRIPTION -This function will alter SQL Agent Job ownership to match a specified login if their -current owner does not match the target login. By default, the target login will -be 'sa', but the fuction will allow the user to specify a different login for -ownership. The user can also apply this to all jobs or only to a select list -of jobs (passed as either a comma separated list or a string array). - -Best practice reference: http://sqlmag.com/blog/sql-server-tip-assign-ownership-jobs-sysadmin-account - -.NOTES -Original Author: Michael Fal (@Mike_Fal), http://mikefal.net - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.PARAMETER SqlServer -SQLServer name or SMO object representing the SQL Server to connect to. This can be a -collection and recieve pipeline input - -.PARAMETER SqlCredential -PSCredential object to connect under. If not specified, currend Windows login will be used. - -.PARAMETER Jobs -Auto-populated list of Jobs to apply changes to. Will accept a comma separated list or a string array. - -.PARAMETER Exclude -Jobs to exclude - -.PARAMETER TargetLogin -Specific login that you wish to check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.LINK -https://dbatools.io/Set-DbaJobOwner - -.EXAMPLE -Set-DbaJobOwner -SqlServer localhost - -Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa. - -.EXAMPLE -Set-DbaJobOwner -SqlServer localhost -TargetLogin DOMAIN\account - -Sets SQL Agent Job owner to sa on all jobs where the owner does not match 'DOMAIN\account'. Note -that TargetLogin must be a valid security principal that exists on the target server. - -.EXAMPLE -Set-DbaJobOwner -SqlServer localhost -Job job1, job2 - -Sets SQL Agent Job owner to 'sa' on the job1 and job2 jobs if their current owner does not match 'sa'. - -.EXAMPLE -'sqlserver','sql2016' | Set-DbaJobOwner - -Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa on both sqlserver and sql2016. - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object[]]$SqlServer, - [object]$SqlCredential, - [string]$TargetLogin - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlJobs -SqlServer $SqlServer[0] -SqlCredential $SourceSqlCredential } } - - BEGIN - { - $jobs = $psboundparameters.Jobs - $exclude = $psboundparameters.Exclude - } - - PROCESS - { - foreach ($servername in $sqlserver) - { - #connect to the instance - Write-Verbose "Connecting to $servername" - $server = Connect-SqlServer $servername -SqlCredential $SqlCredential - - # dynamic sa name for orgs who have changed their sa name - if ($psboundparameters.TargetLogin.length -eq 0) - { - $TargetLogin = ($server.logins | Where-Object { $_.id -eq 1 }).Name - } - - #Validate login - if (($server.Logins.Name) -notcontains $TargetLogin) - { - if ($sqlserver.count -eq 1) - { - throw "Invalid login: $TargetLogin" - } - else - { - Write-Warning "$TargetLogin is not a valid login on $servername. Moving on." - Continue - } - } - - if ($server.logins[$TargetLogin].LoginType -eq 'WindowsGroup') - { - throw "$TargetLogin is a Windows Group and can not be a job owner." - } - - #Get database list. If value for -Jobs is passed, massage to make it a string array. - #Otherwise, use all jobs on the instance where owner not equal to -TargetLogin - Write-Verbose "Gathering jobs to update" - - if ($Jobs.Length -gt 0) - { - $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $TargetLogin -and $jobs -contains $_.Name } - } - else - { - $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $TargetLogin } - } - - if ($Exclude.Length -gt 0) - { - $jobcollection = $jobcollection | Where-Object { $Exclude -notcontains $_.Name } - } - - Write-Verbose "Updating $($jobcollection.Count) job(s)." - foreach ($j in $jobcollection) - { - $jobname = $j.name - - If ($PSCmdlet.ShouldProcess($servername, "Setting job owner for $jobname to $TargetLogin")) - { - try - { - Write-Output "Setting job owner for $jobname to $TargetLogin on $servername" - #Set job owner to $TargetLogin (default 'sa') - $j.OwnerLoginName = $TargetLogin - $j.Alter() - } - catch - { - # write-exception writes the full exception to file - Write-Exception $_ - throw $_ - } - } - } - } - } - - END - { - if ($jobcollection.count -eq 0) - { - Write-Output "Lookin' good! Nothing to do." - } - - Write-Verbose "Closing connection" - $server.ConnectionContext.Disconnect() - } +function Set-DbaJobOwner { + <# + .SYNOPSIS + Sets SQL Agent job owners with a desired login if jobs do not match that owner. + + .DESCRIPTION + This function alters SQL Agent Job ownership to match a specified login if their current owner does not match the target login. By default, the target login will be 'sa', but the the user may specify a different login for ownership. This be applied to all jobs or only to a select collection of jobs. + + Best practice reference: http://sqlmag.com/blog/sql-server-tip-assign-ownership-jobs-sysadmin-account + + .NOTES + Tags: Agent, Job + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .PARAMETER SqlInstance + Specifies the SQL Server instance(s) to scan. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Job + Specifies the job(s) to process. Options for this list are auto-populated from the server. If unspecified, all jobs will be processed. + + .PARAMETER ExcludeJob + Specifies the job(s) to exclude from processing. Options for this list are auto-populated from the server. + + .PARAMETER Login + Specifies the login that you wish check for ownership. This defaults to 'sa' or the sysadmin name if sa was renamed. This must be a valid security principal which exists on the target server. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .LINK + https://dbatools.io/Set-DbaJobOwner + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa. + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost -Login DOMAIN\account + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match 'DOMAIN\account'. Note + that Login must be a valid security principal that exists on the target server. + + .EXAMPLE + Set-DbaJobOwner -SqlInstance localhost -Job job1, job2 + + Sets SQL Agent Job owner to 'sa' on the job1 and job2 jobs if their current owner does not match 'sa'. + + .EXAMPLE + 'sqlserver','sql2016' | Set-DbaJobOwner + + Sets SQL Agent Job owner to sa on all jobs where the owner does not match sa on both sqlserver and sql2016. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [Alias("Jobs")] + [object[]]$Job, + [object[]]$ExcludeJob, + [Alias("TargetLogin")] + [string]$Login, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + foreach ($servername in $SqlInstance) { + #connect to the instance + Write-Message -Level Verbose -Message "Connecting to $servername." + $server = Connect-SqlInstance $servername -SqlCredential $SqlCredential + + # dynamic sa name for orgs who have changed their sa name + if (!$Login) { + $Login = ($server.logins | Where-Object { $_.id -eq 1 }).Name + } + + #Validate login + if (($server.Logins.Name) -notcontains $Login) { + if ($SqlInstance.count -eq 1) { + throw -Message "Invalid login: $Login." + } + else { + Write-Message -Level Warning -Message "$Login is not a valid login on $servername. Moving on." + Continue + } + } + + if ($server.logins[$Login].LoginType -eq 'WindowsGroup') { + throw "$Login is a Windows Group and can not be a job owner." + } + + #Get database list. If value for -Job is passed, massage to make it a string array. + #Otherwise, use all jobs on the instance where owner not equal to -TargetLogin + Write-Message -Level Verbose -Message "Gathering jobs to update." + + if ($Job) { + $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $Login -and $Job -contains $_.Name } + } + else { + $jobcollection = $server.JobServer.Jobs | Where-Object { $_.OwnerLoginName -ne $Login } + } + + if ($ExcludeJob) { + $jobcollection = $jobcollection | Where-Object { $ExcludeJob -notcontains $_.Name } + } + + Write-Message -Level Verbose -Message "Updating $($jobcollection.Count) job(s)." + foreach ($j in $jobcollection) { + $jobname = $j.name + + if ($PSCmdlet.ShouldProcess($servername, "Setting job owner for $jobname to $Login")) { + try { + Write-Message -Level Verbose -Message "Setting job owner for $jobname to $Login on $servername." + #Set job owner to $TargetLogin (default 'sa') + $j.OwnerLoginName = $Login + $j.Alter() + } + catch { + Stop-Function -Message "Issue setting job owner on $jobName." -Target $jobName -InnerErrorRecord $_ -Category InvalidOperation + } + } + } + } + } } diff --git a/functions/Set-DbaLogin.ps1 b/functions/Set-DbaLogin.ps1 new file mode 100644 index 000000000000..bb9cb75053ac --- /dev/null +++ b/functions/Set-DbaLogin.ps1 @@ -0,0 +1,372 @@ +function Set-DbaLogin { + + <# + .SYNOPSIS + Set-DbaLogin makes it possible to make changes to one or more logins. + + .DESCRIPTION + Set-DbaLogin will enable you to change the password, unlock, rename, disable or enable, deny or grant login privileges to the login. + It's also possible to add or remove server roles from the login. + + .PARAMETER SqlInstance + SQL Server instance. You must have sysadmin access and server version must be SQL Server version 2000 or greater. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Login + The login that needs to be changed + + .PARAMETER Password + The new password for the login This can be either a credential or a secure string. + + .PARAMETER Unlock + Switch to unlock an account. This will only be used in conjunction with the -Password parameter. + The default is false. + + .PARAMETER MustChange + Does the user need to change his/her password. This will only be used in conjunction with the -Password parameter. + The default is false. + + .PARAMETER NewName + The new name for the login. + + .PARAMETER Disable + Disable the login + + .PARAMETER Enable + Enable the login + + .PARAMETER DenyLogin + Deny access to SQL Server + + .PARAMETER GrantLogin + Grant access to SQL Server + + .PARAMETER PasswordPolicyEnforced + Should the password policy be enforced. + + .PARAMETER AddRole + Add one or more server roles to the login + The following roles can be used "bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin". + + .PARAMETER RemoveRole + Remove one or more server roles to the login + The following roles can be used "bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin". + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Original Author: Sander Stad (@sqlstad, sqlstad.nl) + Tags: Login + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaLogin + + .EXAMPLE + $password = ConvertTo-SecureString "PlainTextPassword" -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential ("username", $password) + Set-DbaLogin -SqlInstance sql1 -Login login1 -Password $cred -Unlock -MustChange + + Set the new password for login1 using a credential, unlock the account and set the option + that the usermust change password at next logon. + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -Enable + + Enable the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1, login2, login3, login4 -Enable + + Enable multiple logins + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1, sql2, sql3 -Login login1, login2, login3, login4 -Enable + + Enable multiple logins on multiple instances + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -Disable + + Disable the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -DenyLogin + + Deny the login to connect to the instance + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -GrantLogin + + Grant the login to connect to the instance + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -PasswordPolicyEnforced + + Enforces the password policy on a login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login login1 -PasswordPolicyEnforced:$false + + Disables enforcement of the password policy on a login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login test -AddRole serveradmin + + Add the server role "serveradmin" to the login + + .EXAMPLE + Set-DbaLogin -SqlInstance sql1 -Login test -RemoveRole bulkadmin + + Remove the server role "bulkadmin" to the login + +#> + + [CmdletBinding()] + + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [parameter(Mandatory = $true)] + [string[]]$Login, + [object]$Password, + [switch]$Unlock, + [switch]$MustChange, + [string]$NewName, + [switch]$Disable, + [switch]$Enable, + [switch]$DenyLogin, + [switch]$GrantLogin, + [switch]$PasswordPolicyEnforced, + [ValidateSet("bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin")] + [string[]]$AddRole, + [ValidateSet("bulkadmin", "dbcreator", "diskadmin", "processadmin", "public", "securityadmin", "serveradmin", "setupadmin", "sysadmin")] + [string[]]$RemoveRole, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + # Check the parameters + if ($Login -eq $NewName) { + Stop-Function -Message "Login name is the same as the value in -NewName" -Target $Login -Continue + } + + if ($Disable -and $Enable) { + Stop-Function -Message "You cannot use both -Enable and -Disable together" -Target $Login -Continue + } + + if ($GrantLogin -and $DenyLogin) { + Stop-Function -Message "You cannot use both -GrantLogin and -DenyLogin together" -Target $Login -Continue + } + + # Check the password + if ($Password) { + switch ($Password.GetType().Name) { + "PSCredential" { $newPassword = $Password.Password} + "SecureString" { $newPassword = $Password} + } + } + else { + } + + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $sqlinstance) { + # Try connecting to the instance + Write-Message -Message "Attempting to connect to $instance" -Level Verbose + try { + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential -MinimumVersion 9 + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + # Get all the logins + $allLogins = $server.Logins | Where-Object {($_.IsSystemObject -eq $false) -and ($_.Name -notlike '##*')} + $logins = $server.Logins | Where-Object {$Login -contains $_.Name} + + # Loop through all the logins + foreach ($l in $logins) { + + # Create the notes + $notes = @() + + # Change the name + if ($NewName) { + # Check if the new name doesn't already exist + if ($allLogins.Name -notcontains $NewName) { + try { + $l.Rename($NewName) + } + catch { + $notes += "Couldn't rename login" + Stop-Function -Message "Something went wrong changing the name for $l" -Target $l -ErrorRecord $_ -Continue + } + } + else { + $notes += "New login name already exists" + Write-Message -Message "New login name $NewName already exists on $instance" -Level Verbose + } + } + + # Change the password + if ($Password) { + try { + $l.ChangePassword($newPassword, $Unlock, $MustChange) + $passwordChanged = $true + } + catch { + $notes += "Couldn't change password" + $passwordChanged = $false + Stop-Function -Message "Something went wrong changing the password for $l" -Target $l -ErrorRecord $_ -Continue + } + } + + # Disable the login + if ($Disable) { + if ($l.IsDisabled) { + Write-Message -Message "Login $l is already disabled" -Level Verbose + } + else { + try { + $l.Disable() + } + catch { + $notes += "Couldn't disable login" + Stop-Function -Message "Something went wrong disabling $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Enable the login + if ($Enable) { + if (-not $l.IsDisabled) { + Write-Message -Message "Login $l is already enabled" -Level Verbose + } + else { + try { + $l.Enable() + } + catch { + $notes += "Couldn't enable login" + Stop-Function -Message "Something went wrong enabling $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Deny access + if ($DenyLogin) { + if ($l.DenyWindowsLogin) { + Write-Message -Message "Login $l already has login access denied" -Level Verbose + } + else { + $l.DenyWindowsLogin = $true + } + } + + # Grant access + if ($GrantLogin) { + if (-not $l.DenyWindowsLogin) { + Write-Message -Message "Login $l already has login access granted" -Level Verbose + } + else { + $l.DenyWindowsLogin = $false + } + } + + # Enforce password policy + if (Test-Bound PasswordPolicyEnforced) { + if ($l.PasswordPolicyEnforced -eq $PasswordPolicyEnforced) { + Write-Message -Message ("Login $l password policy is already set to " + $l.PasswordPolicyEnforced) -Level Verbose + } else { + $l.PasswordPolicyEnforced = $PasswordPolicyEnforced + } + } + + # Add server roles to login + if ($AddRole) { + # Loop through each of the roles + foreach ($role in $AddRole) { + try { + $l.AddToRole($role) + } + catch { + $notes += "Couldn't add role $role" + Stop-Function -Message "Something went wrong adding role $role to $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Remove server roles from login + if ($RemoveRole) { + # Loop through each of the roles + foreach ($role in $RemoveRole) { + try { + $server.Roles[$role].DropMember($l.Name) + } + catch { + $notes += "Couldn't remove role $role" + Stop-Function -Message "Something went wrong removing role $role to $l" -Target $l -ErrorRecord $_ -Continue + } + } + } + + # Alter the login to make the changes + $l.Alter() + + # Retrieve the server roles for the login + $roles = Get-DbaRoleMember -SqlInstance $instance -IncludeServerLevel | Where-Object {$null -eq $_.Database -and $_.Member -eq $l.Name} + + # Check if there were any notes to include in the results + if ($notes) { + $notes = $notes | Get-Unique + $notes = $notes -Join ';' + } + else { + $notes = $null + } + + # Return the results + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + LoginName = $l.Name + DenyLogin = $l.DenyWindowsLogin + IsDisabled = $l.IsDisabled + IsLocked = $l.IsLocked + PasswordPolicyEnforced = $l.PasswordPolicyEnforced + MustChangePassword = $l.MustChangePassword + PasswordChanged = $passwordChanged + ServerRole = $roles.Role -join "," + Notes = $notes + } | Select-DefaultView -ExcludeProperty Login + + } # end for each login + + } # end for each instance + + } # end process + + end { + if (Test-FunctionInterrupt) { return } + Write-Message -Message "Finished changing login(s)" -Level Verbose + } + + +} \ No newline at end of file diff --git a/functions/Set-DbaMaxDop.ps1 b/functions/Set-DbaMaxDop.ps1 index 19e486ef8eb3..0f3a25df9534 100644 --- a/functions/Set-DbaMaxDop.ps1 +++ b/functions/Set-DbaMaxDop.ps1 @@ -1,314 +1,266 @@ -Function Set-DbaMaxDop -{ -<# -.SYNOPSIS -Sets SQL Server max dop then displays information relating to SQL Server Max DOP configuration settings. Works on SQL Server 2005-2016. +function Set-DbaMaxDop { + <# + .SYNOPSIS + Sets SQL Server maximum degree of parallelism (Max DOP), then displays information relating to SQL Server Max DOP configuration settings. Works on SQL Server 2005 and higher. -.DESCRIPTION -Uses the Test-DbaMaxDop command to get the recommended value if -MaxDop parameter is not specified. + .DESCRIPTION + Uses the Test-DbaMaxDop command to get the recommended value if -MaxDop parameter is not specified. -These are just general recommendations for SQL Server and are a good starting point for setting the "max degree of parallelism" option. -You can set MaxDop database scoped configurations if the server is version 2016. + These are just general recommendations for SQL Server and are a good starting point for setting the "max degree of parallelism" option. -THIS CODE IS PROVIDED "AS IS", WITH NO WARRANTIES. + You can set MaxDop database scoped configurations if the server is version 2016 or higher -.PARAMETER SqlServer -Allows you to specify a comma separated list of servers to query. + .PARAMETER SqlInstance + The SQL Server instance to connect to. -.PARAMETER SqlCredential -Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) -$cred = Get-Credential, this pass this $cred to the param. + .PARAMETER Database + Specifies one or more databases to process. Options for this list are auto-populated from the server. If unspecified, all databases will be processed. -Windows Authentication will be used if DestinationSqlCredential is not specified. To connect as a different Windows user, run PowerShell as that user. + .PARAMETER ExcludeDatabase + Specifies one or more databases to exclude from processing. Options for this list are auto-populated from the server -.PARAMETER MaxDop -Allows you to specify the MaxDop value that you want to use. + .PARAMETER MaxDop + Specifies the Max DOP value to set. -.PARAMETER AllDatabases -This is a parameter that was included so you can set MaxDop value to all databases. Only valid when using on SQL Server 2016 instances. + .PARAMETER AllDatabases + If this switch is enabled, Max DOP will be set on all databases. This switch is only useful on SQL Server 2016 and higher. -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. + .PARAMETER Collection + If Test-SQLMaxDop has been executed prior to this function, the results may be passed in via this parameter. -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. -.PARAMETER Collection -Results of Test-SQLMaxDop to be passed into the command + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. -.NOTES -Author : Claudio Silva (@claudioessilva) -Requires: sysadmin access on SQL Servers + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire + .NOTES + Tags: MaxDop, SpConfigure + Author: Claudio Silva (@claudioessilva) -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + .LINK + https://dbatools.io/Set-DbaMaxDop -You should have received a copy of the GNU General Public License along with this program. If not, see . + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2008, sql2012 -.LINK -https://dbatools.io/Set-DbaMaxDop + Sets Max DOP to the recommended value for servers sql2008 and sql2012. -.EXAMPLE -Set-DbaMaxDop -SqlServer sql2008, sql2012 + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2014 -MaxDop 4 -Set recommended Max DOP setting for servers sql2008 and sql2012. + Sets Max DOP to 4 for server sql2014. -.EXAMPLE -Set-DbaMaxDop -SqlServer sql2014 -MaxDop 4 + .EXAMPLE + Test-DbaMaxDop -SqlInstance sql2008 | Set-DbaMaxDop -Set Max DOP setting to 4 for server sql2014. + Gets the recommended Max DOP from Test-DbaMaxDop and applies it to to sql2008. -.EXAMPLE -Test-DbaMaxDop -SqlServer sql2008 | Set-DbaMaxDop + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2016 -Database db1 -Get Max DOP recommended setting from Test-DbaMaxDop and applies to sql2008 instance + Set recommended Max DOP for database db1 on server sql2016. -.EXAMPLE -Set-DbaMaxDop -SqlServer sql2016 -Databases db1 + .EXAMPLE + Set-DbaMaxDop -SqlInstance sql2016 -AllDatabases -Set recommended Max DOP setting database db1 on server sql2016. + Set recommended Max DOP for all databases on server sql2016. -.EXAMPLE -Set-DbaMaxDop -SqlServer sql2016 -AllDatabases - -Set recommended Max DOP setting for all databases on server sql2016. - - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] - [Alias("ServerInstance", "SqlInstance", "SqlServers")] - [string[]]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential, - [int]$MaxDop = -1, - [Parameter(ValueFromPipeline = $True)] - [object]$Collection, + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, + [Alias("Databases")] + [object[]]$Database, + [object[]]$ExcludeDatabase, + [int]$MaxDop = -1, + [Parameter(ValueFromPipeline = $True)] + [object]$Collection, [Alias("All")] - [switch]$AllDatabases - ) - - DynamicParam { if ($SqlServer) { return Get-ParamSqlDatabases -SqlServer $SqlServer[0] -SqlCredential $SourceSqlCredential } } - - BEGIN - { - $databases = $psboundparameters.Databases - - if ($databases -gt 0 -and $AllDatabases) - { - throw "-Databases and -AllDatabases are mutually exclusive. Please choose only one. Quitting" - } - - $processed = New-Object System.Collections.ArrayList + [switch]$AllDatabases, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + $processed = New-Object System.Collections.ArrayList $results = @() - } - PROCESS - { + } + process { + if ((Test-Bound -Parameter Database) -and (Test-Bound -Parameter AllDatabases) -and (Test-Bound -Parameter ExcludeDatabase)) { + Stop-Function -Category InvalidArgument -Message "-Database, -AllDatabases and -ExcludeDatabase are mutually exclusive. Please choose only one. Quitting." + return + } + $dbscopedconfiguration = $false - if ($MaxDop -eq -1) - { - $UseRecommended = $true - } - - if ($collection -eq $null) - { - $collection = Test-DbaMaxDop -SqlServer $SqlServer -SqlCredential $SqlCredential -Verbose:$false - } - elseif ($collection.Instance -eq $null) - { - $collection = Test-DbaMaxDop -SqlServer $SqlServer -SqlCredential $SqlCredential -Verbose:$false - } - - $collection | Add-Member -NotePropertyName OldInstanceMaxDopValue -NotePropertyValue 0 - $collection | Add-Member -NotePropertyName OldDatabaseMaxDopValue -NotePropertyValue 0 - - $servers = $collection | Select-Object Instance -Unique - - foreach ($server in $servers) - { - if ($server.Instance -ne $null) - { - $servername = $server.Instance - } - else - { - $servername = $server - } - - Write-Verbose "Attempting to connect to $servername" - try - { - $server = Connect-SqlServer -SqlServer $servername -SqlCredential $SqlCredential - } - catch - { - Write-Warning "Can't connect to $server or access denied. Skipping." - continue - } - - if (!(Test-SqlSa -SqlServer $server)) - { - Write-Error "Not a sysadmin on $server. Skipping." - $server.ConnectionContext.Disconnect() - continue - } - - if ($server.versionMajor -ge 13) - { - - Write-Verbose "Server '$servername' supports Max DOP configuration per database." - - if ($databases -eq 0) - { + if ($MaxDop -eq -1) { + $UseRecommended = $true + } + + if ((Test-Bound -Not -Parameter Collection)) { + $collection = Test-DbaMaxDop -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Verbose:$false + } + elseif ($null -eq $collection.SqlInstance) { + $collection = Test-DbaMaxDop -SqlInstance $sqlinstance -SqlCredential $SqlCredential -Verbose:$false + } + + $collection | Add-Member -Force -NotePropertyName OldInstanceMaxDopValue -NotePropertyValue 0 + $collection | Add-Member -Force -NotePropertyName OldDatabaseMaxDopValue -NotePropertyValue 0 + + #If we have servers 2016 or higher we will have a row per database plus the instance level, getting unique we only run one time per instance + $servers = $collection | Select-Object SqlInstance -Unique + + foreach ($server in $servers) { + $servername = $server.SqlInstance + + Write-Message -Level Verbose -Message "Connecting to $servername" + try { + $server = Connect-SqlInstance -SqlInstance $servername -SqlCredential $SqlCredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $servername -Continue + } + + if (!(Test-SqlSa -SqlInstance $server)) { + Stop-Function -Message "Not a sysadmin on $server. Skipping." -Category PermissionDenied -ErrorRecord $_ -Target $currentServer -Continue + } + + if ($server.versionMajor -ge 13) { + Write-Message -Level Verbose -Message "Server '$servername' supports Max DOP configuration per database." + + if ((Test-Bound -Not -Parameter Database) -and (Test-Bound -Not -Parameter ExcludeDatabase)) { #Set at instance level $collection = $collection | Where-Object { $_.DatabaseMaxDop -eq "N/A" } } - else - { + else { $dbscopedconfiguration = $true - if (!$AllDatabases -and $databases -gt 0) - { - - $collection = $collection | Where-Object { $_.Database -in $databases } + if ((Test-Bound -Not -Parameter AllDatabases) -and (Test-Bound -Parameter Database)) { + $collection = $collection | Where-Object { $_.Database -in $Database } } - else - { - if ($AllDatabases) - { + elseif ((Test-Bound -Not -Parameter AllDatabases) -and (Test-Bound -Parameter ExcludeDatabase)) { + $collection = $collection | Where-Object { $_.Database -notin $ExcludeDatabase } + } + else { + if (Test-Bound -Parameter AllDatabases) { $collection = $collection | Where-Object { $_.DatabaseMaxDop -ne "N/A" } } - else - { + else { $collection = $collection | Where-Object { $_.DatabaseMaxDop -eq "N/A" } $dbscopedconfiguration = $false } } } } - else - { - if ($databases -gt 0 -or $AllDatabases) - { - Write-Warning "Server '$servername' (v$($server.versionMajor)) does not support Max DOP configuration at the database level. Remember that this option is only available from SQL Server 2016 (v13). Run the command again without using database related parameters. Skipping." - Continue - } + else { + if ((Test-Bound -Parameter database) -or (Test-Bound -Parameter AllDatabases)) { + Write-Message -Level Warning -Message "Server '$servername' (v$($server.versionMajor)) does not support Max DOP configuration at the database level. Remember that this option is only available from SQL Server 2016 (v13). Run the command again without using database related parameters. Skipping." + Continue + } } - foreach ($row in $collection | Where-Object { $_.Instance -eq $servername }) - { - if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.CurrentInstanceMaxDop) -and !($dbscopedconfiguration)) - { - Write-Output "$servername is configured properly :) No change required." + foreach ($row in $collection | Where-Object { $_.SqlInstance -eq $servername }) { + if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.CurrentInstanceMaxDop) -and !($dbscopedconfiguration)) { + Write-Message -Level Verbose -Message "$servername is configured properly. No change required." Continue } - if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.DatabaseMaxDop) -and $dbscopedconfiguration) - { - Write-Output "Database $($row.Database) on $servername is configured properly :) No change required." + if ($UseRecommended -and ($row.RecommendedMaxDop -eq $row.DatabaseMaxDop) -and $dbscopedconfiguration) { + Write-Message -Level Verbose -Message "Database $($row.Database) on $servername is configured properly. No change required." Continue } - $row.OldInstanceMaxDopValue = $row.CurrentInstanceMaxDop - - try - { - if ($UseRecommended) - { - if ($dbscopedconfiguration) - { - $row.OldDatabaseMaxDopValue = $row.DatabaseMaxDop - - if ($resetDatabases) - { - Write-Verbose "Changing $($row.Database) database max DOP to $($row.DatabaseMaxDop)." - $server.Databases["$($row.Database)"].MaxDop = $row.DatabaseMaxDop - } - else - { - Write-Verbose "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $($row.RecommendedMaxDop)" - $server.Databases["$($row.Database)"].MaxDop = $row.RecommendedMaxDop - $row.DatabaseMaxDop = $row.RecommendedMaxDop - } - - } - else - { - Write-Verbose "Changing $server SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $($row.RecommendedMaxDop)" - $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $row.RecommendedMaxDop - $row.CurrentInstanceMaxDop = $row.RecommendedMaxDop - } - } - else - { - if ($dbscopedconfiguration) - { + $row.OldInstanceMaxDopValue = $row.CurrentInstanceMaxDop + + try { + if ($UseRecommended) { + if ($dbscopedconfiguration) { + $row.OldDatabaseMaxDopValue = $row.DatabaseMaxDop + + if ($resetDatabases) { + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP to $($row.DatabaseMaxDop)." + $server.Databases["$($row.Database)"].MaxDop = $row.DatabaseMaxDop + } + else { + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $($row.RecommendedMaxDop)." + $server.Databases["$($row.Database)"].MaxDop = $row.RecommendedMaxDop + $row.DatabaseMaxDop = $row.RecommendedMaxDop + } + + } + else { + Write-Message -Level Verbose -Message "Changing $server SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $($row.RecommendedMaxDop)." + $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $row.RecommendedMaxDop + $row.CurrentInstanceMaxDop = $row.RecommendedMaxDop + } + } + else { + if ($dbscopedconfiguration) { $row.OldDatabaseMaxDopValue = $row.DatabaseMaxDop - Write-Verbose "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $MaxDop" - $server.Databases["$($row.Database)"].MaxDop = $MaxDop - $row.DatabaseMaxDop = $MaxDop - } - else - { - Write-Verbose "Changing $servername SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $MaxDop" - $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $MaxDop - $row.CurrentInstanceMaxDop = $MaxDop - } - } - - if ($dbscopedconfiguration) - { - if ($Pscmdlet.ShouldProcess($row.Database, "Setting max dop on database")) - { - $server.Databases["$($row.Database)"].Alter() - } - } - else - { - if ($Pscmdlet.ShouldProcess($servername, "Setting max dop on instance")) - { - $server.Configuration.Alter() - } - } - - $results += [pscustomobject]@{ - Instance = $row.Instance - InstanceVersion = $row.InstanceVersion - Database = $row.Database - DatabaseMaxDop = $row.DatabaseMaxDop - CurrentInstanceMaxDop = $row.CurrentInstanceMaxDop - RecommendedMaxDop = $row.RecommendedMaxDop - OldDatabaseMaxDopValue = $row.OldDatabaseMaxDopValue - OldInstanceMaxDopValue = $row.OldInstanceMaxDopValue - } - } - catch { Write-Error "Could not modify Max Degree of Paralellism for $server." } - } - - $server.ConnectionContext.Disconnect() - } - - } - END - { - - if ($Pscmdlet.ShouldProcess("console", "Showing finished message")) - { - if ($dbscopedconfiguration) - { - return $results | Select-Object Instance, Database, OldDatabaseMaxDopValue, @{ name = "CurrentDatabaseMaxDopValue"; expression = { $_.DatabaseMaxDop } } - } - else - { - return $results | Select-Object Instance, OldInstanceMaxDopValue, CurrentInstanceMaxDop - } - } + Write-Message -Level Verbose -Message "Changing $($row.Database) database max DOP from $($row.DatabaseMaxDop) to $MaxDop." + $server.Databases["$($row.Database)"].MaxDop = $MaxDop + $row.DatabaseMaxDop = $MaxDop + } + else { + Write-Message -Level Verbose -Message "Changing $servername SQL Server max DOP from $($row.CurrentInstanceMaxDop) to $MaxDop." + $server.Configuration.MaxDegreeOfParallelism.ConfigValue = $MaxDop + $row.CurrentInstanceMaxDop = $MaxDop + } + } + + if ($dbscopedconfiguration) { + if ($Pscmdlet.ShouldProcess($row.Database, "Setting max dop on database")) { + $server.Databases["$($row.Database)"].Alter() + } + } + else { + if ($Pscmdlet.ShouldProcess($servername, "Setting max dop on instance")) { + $server.Configuration.Alter() + } + } + + $results += [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + InstanceVersion = $row.InstanceVersion + Database = $row.Database + DatabaseMaxDop = $row.DatabaseMaxDop + CurrentInstanceMaxDop = $row.CurrentInstanceMaxDop + RecommendedMaxDop = $row.RecommendedMaxDop + OldDatabaseMaxDopValue = $row.OldDatabaseMaxDopValue + OldInstanceMaxDopValue = $row.OldInstanceMaxDopValue + } + } + catch { + Stop-Function -Message "Could not modify Max Degree of Parallelism for $server." -ErrorRecord $_ -Target $server -Continue + } + } + + if ($dbscopedconfiguration) { + Select-DefaultView -InputObject $results -Property InstanceName, Database, OldDatabaseMaxDopValue, @{ name = "CurrentDatabaseMaxDopValue"; expression = { $_.DatabaseMaxDop } } + } + else { + Select-DefaultView -InputObject $results -Property InstanceName, OldInstanceMaxDopValue, CurrentInstanceMaxDop + } + } } -} +} \ No newline at end of file diff --git a/functions/Set-DbaMaxMemory.ps1 b/functions/Set-DbaMaxMemory.ps1 index cb528fca2348..17012ca0afcc 100644 --- a/functions/Set-DbaMaxMemory.ps1 +++ b/functions/Set-DbaMaxMemory.ps1 @@ -1,186 +1,144 @@ -Function Set-DbaMaxMemory -{ - <# - .SYNOPSIS - Sets SQL Server 'Max Server Memory' configuration setting to a new value then displays information this setting. +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Set-DbaMaxMemory { + <# + .SYNOPSIS + Sets SQL Server 'Max Server Memory' configuration setting to a new value then displays information this setting. .DESCRIPTION - Sets SQL Server max memory then displays information relating to SQL Server Max Memory configuration settings. + Sets SQL Server max memory then displays information relating to SQL Server Max Memory configuration settings. - Inspired by Jonathan Kehayias's post about SQL Server Max memory (http://bit.ly/sqlmemcalc), this uses a formula to + Inspired by Jonathan Kehayias's post about SQL Server Max memory (http://bit.ly/sqlmemcalc), this uses a formula to determine the default optimum RAM to use, then sets the SQL max value to that number. - Jonathan notes that the formula used provides a *general recommendation* that doesn't account for everything that may + Jonathan notes that the formula used provides a *general recommendation* that doesn't account for everything that may be going on in your specific environment. - .PARAMETER SqlServer + .PARAMETER SqlInstance Allows you to specify a comma separated list of servers to query. - .PARAMETER MaxMb + .PARAMETER MaxMB Specifies the max megabytes - .PARAMETER SqlCredential - Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: - - $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. - - Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials - being passed as credentials. To connect as a different Windows user, run PowerShell as that user. - - .PARAMETER Collection - Results of Get-DbaMaxMemory to be passed into the command - - .PARAMETER Silent - Replaces user friendly yellow warnings with bloody red exceptions of doom! - Use this if you want the function to throw terminating errors you want to catch. - + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. - + .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. - .EXAMPLE + .NOTES + Tags: MaxMemory, Memory + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaMaxMemory + + .EXAMPLE Set-DbaMaxMemory sqlserver1 Set max memory to the recommended MB on just one server named "sqlserver1" - .EXAMPLE - Set-DbaMaxMemory -SqlServer sqlserver1 -MaxMb 2048 + .EXAMPLE + Set-DbaMaxMemory -SqlInstance sqlserver1 -MaxMB 2048 Explicitly max memory to 2048 MB on just one server, "sqlserver1" - .EXAMPLE - Get-SqlRegisteredServerName -SqlServer sqlserver| Test-DbaMaxMemory | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } | Set-DbaMaxMemory + .EXAMPLE + Get-DbaRegisteredServer -SqlInstance sqlserver | Test-DbaMaxMemory | Where-Object { $_.SqlMaxMB -gt $_.TotalMB } | Set-DbaMaxMemory - Find all servers in SQL Server Central Management server that have Max SQL memory set to higher than the total memory + Find all servers in SQL Server Central Management server that have Max SQL memory set to higher than the total memory of the server (think 2147483647), then pipe those to Set-DbaMaxMemory and use the default recommendation. - - .NOTES - dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) - Copyright (C) 2016 Chrissy LeMaire - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . - - .LINK - https://dbatools.io/Set-DbaMaxMemory #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - Param ( + param ( [Parameter(Position = 0)] - [Alias("ServerInstance", "SqlInstance", "SqlServers", 'ComputerName')] - [object] - $SqlServer, - + [Alias("ServerInstance", "SqlServer", "SqlServers", "ComputerName")] + [DbaInstanceParameter[]]$SqlInstance, + [Alias("Credential")] + [PSCredential]$SqlCredential, [Parameter(Position = 1)] - [int] - $MaxMb, - - [Parameter(ValueFromPipeline = $True)] - [object] - $Collection, - - [Alias('Credential')] - [System.Management.Automation.PSCredential] - $SqlCredential, - - [switch] - $Silent + [int]$MaxMB, + [Alias('Silent')] + [switch]$EnableException ) - Process - { - if ($SqlServer.length -eq 0 -and $collection -eq $null) - { - Stop-Function -Silent $Silent -Category InvalidArgument -Message "You must specify a server list source using -SqlServer or you can pipe results from Test-DbaMaxMemory" + begin { + if ((Test-Bound -Not -Parameter SqlInstance) -and (Test-Bound -Not -Parameter Collection)) { + Stop-Function -Category InvalidArgument -Message "You must specify a server list source using -SqlInstance or you can pipe results from Test-DbaMaxMemory" return } - - if ($MaxMB -eq 0) - { + + if ($MaxMB -eq 0) { $UseRecommended = $true } - - if ($Collection -eq $null) - { - $Collection = Test-DbaMaxMemory -SqlServer $SqlServer -SqlCredential $SqlCredential - } - - # We ignore errors, because this will error if we pass the same collection items twice. - # Given that it is an engine internal command, there is no other plausible error it could encounter. - $Collection | Add-Member -NotePropertyName OldMaxValue -NotePropertyValue 0 -ErrorAction Ignore - - foreach ($row in $Collection) - { - if ($row.server -eq $null) - { - $row = Test-DbaMaxMemory -sqlserver $row - $row | Add-Member -NotePropertyName OldMaxValue -NotePropertyValue 0 + } + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $SqlCredential } - - Write-Verbose "Attempting to connect to $($row.server)" - - try - { - $server = Connect-SqlServer -SqlServer $row.server -SqlCredential $SqlCredential -ErrorAction Stop + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue } - catch - { - Stop-Function -Message "Can't connect to $($row.server) or access denied. Skipping." -Silent $Silent -Category ConnectionError -InnerErrorRecord $_ -Target $row -Continue + + if (!(Test-SqlSa -SqlInstance $server)) { + Stop-Function -Message "Not a sysadmin on $server. Skipping." -Category PermissionDenied -ErrorRecord $_ -Target $server -Continue + } + + try { + $currentServer = Test-DbaMaxMemory -SqlInstance $server + Add-Member -Force -InputObject $currentServer -NotePropertyName OldMaxValue -NotePropertyValue 0 + $currentServer.OldMaxValue = $currentServer.SqlMaxMB } - - if (!(Test-SqlSa -SqlServer $server)) - { - Stop-Function -Message "Not a sysadmin on $($row.server). Skipping." -Silent $Silent -Category PermissionDenied -InnerErrorRecord $_ -Target $row -Continue + catch { + Stop-Function -Message "Issue collecting memory information on $server" -Target $server -ErrorRecord $_ -InnerException $_.Exception -Continue } - - $row.OldMaxValue = $row.SqlMaxMB - - try - { - if ($UseRecommended) - { - Write-Verbose "Changing $($row.server) SQL Server max from $($row.SqlMaxMB) to $($row.RecommendedMB) MB" - - if ($row.RecommendedMB -eq 0 -or $row.RecommendedMB -eq $null) - { - $maxmem = (Test-DbaMaxMemory -SqlServer $server).RecommendedMB - Write-Warning $maxmem - $server.Configuration.MaxServerMemory.ConfigValue = $maxmem + + try { + if ($UseRecommended) { + Write-Message -Level Verbose -Message "Change $server SQL Server Max Memory from $($currentServer.SqlMaxMB) to $($currentServer.RecommendedMB) MB" + + if ($currentServer.RecommendedMB -eq 0 -or $null -eq $currentServer.RecommendedMB) { + $maxMem = (Test-DbaMaxMemory -SqlInstance $server).RecommendedMB + Write-Message -Level VeryVerbose -Message "Max memory recommended: $maxMem" + $server.Configuration.MaxServerMemory.ConfigValue = $maxMem } - else - { - - $server.Configuration.MaxServerMemory.ConfigValue = $row.RecommendedMB + else { + $server.Configuration.MaxServerMemory.ConfigValue = $currentServer.RecommendedMB } } - else - { - Write-Verbose "Changing $($row.server) SQL Server max from $($row.SqlMaxMB) to $MaxMB MB" + else { + Write-Message -Level Verbose -Message "Change $server SQL Server Max Memory from $($currentServer.SqlMaxMB) to $MaxMB MB" $server.Configuration.MaxServerMemory.ConfigValue = $MaxMB } - if ($PSCmdlet.ShouldProcess($row.Server, "Changing maximum memory from $($row.OldMaxValue) to $($server.Configuration.MaxServerMemory.ConfigValue)")) - { - try - { + if ($PSCmdlet.ShouldProcess($server, "Change Max Memory from $($currentServer.OldMaxValue) to $($server.Configuration.MaxServerMemory.ConfigValue)")) { + try { $server.Configuration.Alter() - $row.SqlMaxMB = $server.Configuration.MaxServerMemory.ConfigValue + $currentServer.SqlMaxMB = $server.Configuration.MaxServerMemory.ConfigValue } - catch - { - Stop-Function -Message "Failed to apply configuration change for $($row.Server): $($_.Exception.Message)" -Silent $Silent -InnerErrorRecord $_ -Target $row -Continue + catch { + Stop-Function -Message "Failed to apply configuration change for $server" -ErrorRecord $_ -Target $server -Continue } } } - catch - { - Stop-Function -Message "Could not modify Max Server Memory for $($row.server): $($_.Exception.Message)" -Silent $Silent -InnerErrorRecord $_ -Target $row -Continue + catch { + Stop-Function -Message "Could not modify Max Server Memory for $server" -ErrorRecord $_ -Target $server -Continue } - - $row | Select-Object Server, TotalMB, OldMaxValue, @{ name = "CurrentMaxValue"; expression = { $_.SqlMaxMB } } + + Add-Member -InputObject $currentServer -Force -MemberType NoteProperty -Name CurrentMaxValue -Value $currentServer.SqlMaxMB + Select-DefaultView -InputObject $currentServer -Property ComputerName, InstanceName, SqlInstance, TotalMB, OldMaxValue, CurrentMaxValue } } -} +} \ No newline at end of file diff --git a/functions/Set-DbaNetworkCertificate.ps1 b/functions/Set-DbaNetworkCertificate.ps1 new file mode 100644 index 000000000000..a7f5af72e328 --- /dev/null +++ b/functions/Set-DbaNetworkCertificate.ps1 @@ -0,0 +1,212 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# + +function Set-DbaNetworkCertificate { + <# + .SYNOPSIS + Sets the network certificate for SQL Server instance + + .DESCRIPTION + Sets the network certificate for SQL Server instance. This setting is found in Configuration Manager. + + This command also grants read permissions for the service account on the certificate's private key. + + References: + http://sqlmag.com/sql-server/7-steps-ssl-encryption + https://azurebi.jppp.org/2016/01/23/using-lets-encrypt-certificates-for-secure-sql-server-connections/ + https://blogs.msdn.microsoft.com/sqlserverfaq/2016/09/26/creating-and-registering-ssl-certificates/ + + .PARAMETER SqlInstance + The target SQL Server - defaults to localhost. + + .PARAMETER Credential + Allows you to login to the computer (not sql instance) using alternative credentials. + + .PARAMETER Certificate + The target certificate object + + .PARAMETER Thumbprint + The thumbprint of the target certificate + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .EXAMPLE + New-DbaComputerCertificate | Set-DbaNetworkCertificate -SqlInstance localhost\SQL2008R2SP2 + + Creates and imports a new certificate signed by an Active Directory CA on localhost then sets the network certificate for the SQL2008R2SP2 to that newly created certificate. + + .EXAMPLE + Set-DbaNetworkCertificate -SqlInstance sql1\SQL2008R2SP2 -Thumbprint 1223FB1ACBCA44D3EE9640F81B6BA14A92F3D6E2 + + Sets the network certificate for the SQL2008R2SP2 instance to the certificate with the thumbprint of 1223FB1ACBCA44D3EE9640F81B6BA14A92F3D6E2 in LocalMachine\My on sql1 + + .NOTES + Tags: Certificate + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low", DefaultParameterSetName = 'Default')] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "ComputerName")] + [DbaInstanceParameter[]] + $SqlInstance = $env:COMPUTERNAME, + + [PSCredential] + + $Credential, + + [parameter(Mandatory, ParameterSetName = "Certificate", ValueFromPipeline)] + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, + + [parameter(Mandatory, ParameterSetName = "Thumbprint")] + [string] + $Thumbprint, + + [switch] + [Alias('Silent')]$EnableException + ) + + process { + if (Test-FunctionInterrupt) { return } + $Certificate + if (!$Certificate -and !$Thumbprint) { + Stop-Function -Message "You must specify a certificate or thumbprint" + return + } + + if (!$Thumbprint) { + Write-Message -Level SomewhatVerbose -Message "Getting thumbprint" + $Thumbprint = $Certificate.Thumbprint + } + + foreach ($instance in $sqlinstance) { + Write-Message -Level VeryVerbose -Message "Processing $instance" -Target $instance + $null = Test-ElevationRequirement -ComputerName $instance -Continue + + Write-Message -Level Verbose -Message "Resolving hostname" + $resolved = $null + $resolved = Resolve-DbaNetworkName -ComputerName $instance -Turbo + + if ($null -eq $resolved) { + Stop-Function -Message "Can't resolve $instance" -Target $instance -Continue -Category InvalidArgument + } + + $computername = $instance.ComputerName + $instancename = $instance.instancename + Write-Message -Level Output -Message "Connecting to SQL WMI on $computername" + + try { + $sqlwmi = Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock { $wmi.Services } -Credential $Credential -ErrorAction Stop | Where-Object DisplayName -eq "SQL Server ($instancename)" + } + catch { + Stop-Function -Message "Failed to access $instance" -Target $instance -Continue -ErrorRecord $_ + } + + if (-not $sqlwmi) { + Stop-Function -Message "Cannot find $instancename on $computerName" -Continue -Category ObjectNotFound -Target $instance + } + + $regroot = ($sqlwmi.AdvancedProperties | Where-Object Name -eq REGROOT).Value + $vsname = ($sqlwmi.AdvancedProperties | Where-Object Name -eq VSNAME).Value + $instancename = $sqlwmi.DisplayName.Replace('SQL Server (', '').Replace(')', '') # Don't clown, I don't know regex :( + $serviceaccount = $sqlwmi.ServiceAccount + + if ([System.String]::IsNullOrEmpty($regroot)) { + $regroot = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'REGROOT' } + $vsname = $sqlwmi.AdvancedProperties | Where-Object { $_ -match 'VSNAME' } + + if (![System.String]::IsNullOrEmpty($regroot)) { + $regroot = ($regroot -Split 'Value\=')[1] + $vsname = ($vsname -Split 'Value\=')[1] + } + else { + Stop-Function -Message "Can't find instance $vsname on $instance" -Continue -Category ObjectNotFound -Target $instance + } + } + + if ([System.String]::IsNullOrEmpty($vsname)) { $vsname = $instance } + + Write-Message -Level Output -Message "Regroot: $regroot" -Target $instance + Write-Message -Level Output -Message "ServiceAcct: $serviceaccount" -Target $instance + Write-Message -Level Output -Message "InstanceName: $instancename" -Target $instance + Write-Message -Level Output -Message "VSNAME: $vsname" -Target $instance + + $scriptblock = { + $regroot = $args[0] + $serviceaccount = $args[1] + $instancename = $args[2] + $vsname = $args[3] + $Thumbprint = $args[4] + + $regpath = "Registry::HKEY_LOCAL_MACHINE\$regroot\MSSQLServer\SuperSocketNetLib" + + $oldthumbprint = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + + $cert = Get-ChildItem Cert:\LocalMachine -Recurse -ErrorAction Stop | Where-Object { $_.Thumbprint -eq $Thumbprint } + + if ($null -eq $cert) { + Write-Warning "Certificate does not exist on $env:COMPUTERNAME" + return + } + + $permission = $serviceaccount, "Read", "Allow" + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyFullPath = $keyPath + $keyName + + $acl = Get-Acl -Path $keyFullPath + $null = $acl.AddAccessRule($accessRule) + Set-Acl -Path $keyFullPath -AclObject $acl + + if ($acl) { + Set-ItemProperty -Path $regpath -Name Certificate -Value $Thumbprint.ToString().ToLower() # to make it compat with SQL config + } + else { + Write-Warning "Read-only permissions could not be granted to certificate" + return + } + + if (![System.String]::IsNullOrEmpty($oldthumbprint)) { + $notes = "Granted $serviceaccount read access to certificate private key. Replaced thumbprint: $oldthumbprint." + } + else { + $notes = "Granted $serviceaccount read access to certificate private key" + } + + $newthumbprint = (Get-ItemProperty -Path $regpath -Name Certificate).Certificate + + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $instancename + SqlInstance = $vsname + ServiceAccount = $serviceaccount + CertificateThumbprint = $newthumbprint + Notes = $notes + } + } + + if ($PScmdlet.ShouldProcess("local", "Connecting to $instanceName to import new cert")) { + try { + Invoke-Command2 -Raw -ComputerName $resolved.fqdn -Credential $Credential -ArgumentList $regroot, $serviceaccount, $instancename, $vsname, $Thumbprint -ScriptBlock $scriptblock -ErrorAction Stop + } + catch { + Stop-Function -Message "Failed to connect to $($resolved.fqdn) using PowerShell remoting!" -ErrorRecord $_ -Target $instance -Continue + } + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaPowerPlan.ps1 b/functions/Set-DbaPowerPlan.ps1 index 6e2318c755d4..83c45a3997b4 100644 --- a/functions/Set-DbaPowerPlan.ps1 +++ b/functions/Set-DbaPowerPlan.ps1 @@ -1,194 +1,177 @@ -Function Set-DbaPowerPlan -{ -<# -.SYNOPSIS -Sets the SQL Server OS's Power Plan. - -.DESCRIPTION -Sets the SQL Server OS's Power Plan. It defaults to High Performance which is Best Practice. - -If your organization uses a custom power plan that is considered best practice, specify -CustomPowerPlan. - -References: -https://support.microsoft.com/en-us/kb/2207548 -http://www.sqlskills.com/blogs/glenn/windows-power-plan-effects-on-newer-intel-processors/ - -.PARAMETER ComputerName -The SQL Server (or server in general) that you're connecting to. The -SqlServer parameter also works. This command handles named instances. - -.PARAMETER PowerPlan -The Power Plan that you wish to use. These are validated to Windows default Power Plans (Power saver, Balanced, High Performance) - -.PARAMETER CustomPowerPlan -If you use a custom power plan instead of Windows default, use CustomPowerPlan - -.PARAMETER WhatIf -Shows what would happen if the command were to run. No actions are actually performed. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.NOTES -Requires: WMI access to servers - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -.LINK -https://dbatools.io/Set-DbaPowerPlan - -.EXAMPLE -Set-DbaPowerPlan -ComputerName sqlserver2014a - -Sets the Power Plan to High Performance. Skips it if its already set. - -.EXAMPLE -Set-DbaPowerPlan -ComputerName sqlcluster -CustomPowerPlan 'Maximum Performance' - -Sets the Power Plan to the custom power plan called "Maximum Performance". Skips it if its already set. - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlInstance", "SqlServer")] - [string[]]$ComputerName, - [ValidateSet('High Performance', 'Balanced', 'Power saver')] - [string]$PowerPlan = 'High Performance', - [string]$CustomPowerPlan - ) - - BEGIN - { - if ($CustomPowerPlan.Length -gt 0) { $PowerPlan = $CustomPowerPlan } - - Function Set-DbaPowerPlan - { - try - { - Write-Verbose "Testing connection to $server and resolving IP address" - $ipaddr = (Test-Connection $server -Count 1 -ErrorAction SilentlyContinue).Ipv4Address | Select-Object -First 1 - - } - catch - { - Write-Warning "Can't connect to $server" - return - } - - try - { - Write-Verbose "Getting Power Plan information from $server" - $query = "Select ElementName from Win32_PowerPlan WHERE IsActive = 'true'" - $currentplan = Get-WmiObject -Namespace Root\CIMV2\Power -ComputerName $ipaddr -Query $query -ErrorAction SilentlyContinue - $currentplan = $currentplan.ElementName - } - catch - { - Write-Warning "Can't connect to WMI on $server" - return - } - - if ($currentplan -eq $null) - { - # the try/catch above isn't working, so make it silent and handle it here. - Write-Warning "Cannot get Power Plan for $server" - return - } - - $planinfo = [PSCustomObject]@{ - Server = $server - PreviousPowerPlan = $currentplan - ActivePowerPlan = $PowerPlan - } - - if ($PowerPlan -ne $currentplan) - { - If ($Pscmdlet.ShouldProcess($server, "Changing Power Plan from $CurrentPlan to $PowerPlan")) - { - try - { - Write-Verbose "Setting Power Plan to $PowerPlan" - $null = (Get-WmiObject -Name root\cimv2\power -ComputerName $ipaddr -Class Win32_PowerPlan -Filter "ElementName='$PowerPlan'").Activate() - } - catch - { - Write-Exception $_ - Write-Warning "Couldn't set Power Plan on $server" - return - } - } - } - else - { - If ($Pscmdlet.ShouldProcess($server, "Stating power plan is already set to $PowerPlan, won't change.")) - { - Write-Warning "PowerPlan on $server is already set to $PowerPlan. Skipping." - } - } - - return $planinfo - } - - - $collection = New-Object System.Collections.ArrayList - $processed = New-Object System.Collections.ArrayList - } - - PROCESS - { - foreach ($server in $ComputerName) - { - if ($server -match 'Server\=') - { - Write-Verbose "Matched that value was piped from Test-DbaPowerPlan" - # I couldn't properly unwrap the output from Test-DbaPowerPlan so here goes. - $lol = $server.Split("\;")[0] - $lol = $lol.TrimEnd("\}") - $lol = $lol.TrimStart("\@\{Server") - # There was some kind of parsing bug here, don't clown - $server = $lol.TrimStart("\=") - } - - if ($server -match '\\') - { - $server = $server.Split('\\')[0] - } - - if ($server -notin $processed) - { - $null = $processed.Add($server) - Write-Verbose "Connecting to $server" - } - else - { - continue - } - - $data = Set-DbaPowerPlan $server - - if ($data.Count -gt 1) - { - $data.GetEnumerator() | ForEach-Object { $null = $collection.Add($_) } - } - else - { - $null = $collection.Add($data) - } - } - } - - END - { - If ($Pscmdlet.ShouldProcess("console", "Showing results")) - { - return $collection - } - } +function Set-DbaPowerPlan { + <# + .SYNOPSIS + Sets the SQL Server OS's Power Plan. + + .DESCRIPTION + Sets the SQL Server OS's Power Plan. Defaults to High Performance which is best practice. + + If your organization uses a custom power plan that is considered best practice, specify -CustomPowerPlan. + + References: + https://support.microsoft.com/en-us/kb/2207548 + http://www.sqlskills.com/blogs/glenn/windows-power-plan-effects-on-newer-intel-processors/ + + .PARAMETER ComputerName + The server(s) to set the Power Plan on. + + .PARAMETER PowerPlan + Specifies the Power Plan that you wish to use. Valid options for this match the Windows default Power Plans of "Power Saver", "Balanced", and "High Performance". + + .PARAMETER CustomPowerPlan + Specifies the name of a custom Power Plan to use. + + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .NOTES + Tags: PowerPlan, OS, Configure + Requires: WMI access to servers + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaPowerPlan + + .EXAMPLE + Set-DbaPowerPlan -ComputerName sqlserver2014a + + Sets the Power Plan to High Performance. Skips it if its already set. + + .EXAMPLE + Set-DbaPowerPlan -ComputerName sqlcluster -CustomPowerPlan 'Maximum Performance' + + Sets the Power Plan to the custom power plan called "Maximum Performance". Skips it if its already set. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer", "SqlInstance")] + [object[]]$ComputerName, + [ValidateSet('High Performance', 'Balanced', 'Power saver')] + [string]$PowerPlan = 'High Performance', + [string]$CustomPowerPlan, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + if ($CustomPowerPlan.Length -gt 0) { + $PowerPlan = $CustomPowerPlan + } + + function Set-DbaPowerPlanInternal { + param($server) + + try { + Write-Message -Level Verbose -Message "Testing connection to $server and resolving IP address." + $ipaddr = (Test-Connection $server -Count 1 -ErrorAction SilentlyContinue).Ipv4Address | Select-Object -First 1 + + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + try { + Write-Message -Level Verbose -Message "Getting Power Plan information from $server." + $query = "Select ElementName from Win32_PowerPlan WHERE IsActive = 'true'" + $currentplan = Get-WmiObject -Namespace Root\CIMV2\Power -ComputerName $ipaddr -Query $query -ErrorAction SilentlyContinue + $currentplan = $currentplan.ElementName + } + catch { + Stop-Function -Message "Can't connect to WMI on $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + if ($null -eq $currentplan) { + # the try/catch above isn't working, so make it silent and handle it here. + Stop-Function -Message "Cannot get Power Plan for $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + + $planinfo = [PSCustomObject]@{ + Server = $server + PreviousPowerPlan = $currentplan + ActivePowerPlan = $PowerPlan + } + + if ($PowerPlan -ne $currentplan) { + if ($Pscmdlet.ShouldProcess($server, "Changing Power Plan from $CurrentPlan to $PowerPlan")) { + try { + Write-Message -Level Verbose -Message "Setting Power Plan to $PowerPlan." + $null = (Get-WmiObject -Name root\cimv2\power -ComputerName $ipaddr -Class Win32_PowerPlan -Filter "ElementName='$PowerPlan'").Activate() + } + catch { + Stop-Function -Message "Couldn't set Power Plan on $server." -Category ConnectionError -ErrorRecord $_ -Target $server + return + } + } + } + else { + if ($Pscmdlet.ShouldProcess($server, "Stating power plan is already set to $PowerPlan, won't change.")) { + Write-Message -Level Verbose -Message "PowerPlan on $server is already set to $PowerPlan. Skipping." + } + } + + return $planinfo + } + + + $collection = New-Object System.Collections.ArrayList + $processed = New-Object System.Collections.ArrayList + } + + process { + foreach ($server in $ComputerName) { + if ($server -match 'Server\=') { + Write-Message -Level Verbose -Message "Matched that value was piped from Test-DbaPowerPlan." + # I couldn't properly unwrap the output from Test-DbaPowerPlan so here goes. + $lol = $server.Split("\;")[0] + $lol = $lol.TrimEnd("\}") + $lol = $lol.TrimStart("\@\{Server") + # There was some kind of parsing bug here, don't clown + $server = $lol.TrimStart("\=") + } + + if ($server -match '\\') { + $server = $server.Split('\\')[0] + } + + if ($server -notin $processed) { + $null = $processed.Add($server) + Write-Message -Level Verbose -Message "Connecting to $server." + } + else { + continue + } + + $data = Set-DbaPowerPlanInternal $server + + if ($data.Count -gt 1) { + $data.GetEnumerator() | ForEach-Object { $null = $collection.Add($_) } + } + else { + $null = $collection.Add($data) + } + } + } + + end { + If ($Pscmdlet.ShouldProcess("console", "Showing results")) { + return $collection + } + } } diff --git a/functions/Set-DbaPrivilege.ps1 b/functions/Set-DbaPrivilege.ps1 new file mode 100644 index 000000000000..1448187ff18a --- /dev/null +++ b/functions/Set-DbaPrivilege.ps1 @@ -0,0 +1,154 @@ +function Set-DbaPrivilege { + <# + .SYNOPSIS + Adds the SQL Service account to local privileges on one or more computers. + + .DESCRIPTION + Adds the SQL Service account to local privileges 'Lock Pages in Memory', 'Instant File Initialization', 'Logon as Batch' on one or more computers. + + Requires Local Admin rights on destination computer(s). + + .PARAMETER ComputerName + The SQL Server (or server in general) that you're connecting to. This command handles named instances. + + .PARAMETER Credential + Credential object used to connect to the computer as a different user. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Type + Use this to choose the privilege(s) to which you want to add the SQL Service account. + Accepts 'IFI', 'LPIM' and/or 'BatchLogon' for local privileges 'Instant File Initialization', 'Lock Pages in Memory' and 'Logon as Batch'. + + .NOTES + Author: Klaas Vandenberghe ( @PowerDBAKlaas ) + Tags: Privilege + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaPrivilege + + .EXAMPLE + Set-DbaPrivilege -ComputerName sqlserver2014a -Type LPIM,IFI + + Adds the SQL Service account(s) on computer sqlserver2014a to the local privileges 'SeManageVolumePrivilege' and 'SeLockMemoryPrivilege'. + + .EXAMPLE + 'sql1','sql2','sql3' | Set-DbaPrivilege -Type IFI + + Adds the SQL Service account(s) on computers sql1, sql2 and sql3 to the local privilege 'SeManageVolumePrivilege'. + + #> + [CmdletBinding()] + Param ( + [parameter(ValueFromPipeline)] + [Alias("cn", "host", "Server")] + [dbainstanceparameter[]]$ComputerName = $env:COMPUTERNAME, + [PSCredential]$Credential, + [Parameter(Mandatory = $true)] + [ValidateSet('IFI', 'LPIM', 'BatchLogon')] + [string[]]$Type, + [switch][Alias('Silent')] + $EnableException + ) + + begin { + $ResolveAccountToSID = @" +function Convert-UserNameToSID ([string] `$Acc ) { +`$objUser = New-Object System.Security.Principal.NTAccount(`"`$Acc`") +`$strSID = `$objUser.Translate([System.Security.Principal.SecurityIdentifier]) +`$strSID.Value +} +"@ + $ComputerName = $ComputerName.ComputerName | Select-Object -Unique + } + process { + foreach ($computer in $ComputerName) { + try { + Write-Message -Level Verbose -Message "Connecting to $computer" + $null = Test-ElevationRequirement -ComputerName $Computer -Continue + if (Test-PSRemoting -ComputerName $Computer) { + Write-Message -Level Verbose -Message "Exporting Privileges on $Computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); secedit /export /cfg $temp\secpolByDbatools.cfg > $NULL; + } + Write-Message -Level Verbose -Message "Getting SQL Service Accounts on $computer" + $SQLServiceAccounts = (Get-DbaSqlService -ComputerName $computer -Type Engine).StartName + if ($SQLServiceAccounts.count -ge 1) { + Write-Message -Level Verbose -Message "Setting Privileges on $Computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -Verbose -ArgumentList $ResolveAccountToSID, $SQLServiceAccounts, $BatchLogon, $IFI, $LPIM -ScriptBlock { + [CmdletBinding()] + Param ($ResolveAccountToSID, + $SQLServiceAccounts, + $BatchLogon, + $IFI, + $LPIM) + . ([ScriptBlock]::Create($ResolveAccountToSID)) + $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); + $tempfile = "$temp\secpolByDbatools.cfg" + if ('BatchLogon' -in $Type) { + $BLline = Get-Content $tempfile | Where-Object { $_ -match "SeBatchLogonRight" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($BLline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeBatchLogonRight = ", "SeBatchLogonRight = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Batch Logon Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Batch Logon Privilege on $env:ComputerName" + } + } + } + if ('IFI' -in $Type) { + $IFIline = Get-Content $tempfile | Where-Object { $_ -match "SeManageVolumePrivilege" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($IFIline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeManageVolumePrivilege = ", "SeManageVolumePrivilege = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Instant File Initialization Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Instant File Initialization Privilege on $env:ComputerName" + } + } + } + if ('LPIM' -in $Type) { + $LPIMline = Get-Content $tempfile | Where-Object { $_ -match "SeLockMemoryPrivilege" } + ForEach ($acc in $SQLServiceAccounts) { + $SID = Convert-UserNameToSID -Acc $acc; + if ($LPIMline -notmatch $SID) { + (Get-Content $tempfile) -replace "SeLockMemoryPrivilege = ", "SeLockMemoryPrivilege = *$SID," | + Set-Content $tempfile + Write-Verbose "Added $acc to Lock Pages in Memory Privileges on $env:ComputerName" + } + else { + Write-Warning "$acc already has Lock Pages in Memory Privilege on $env:ComputerName" + } + } + } + $null = secedit /configure /cfg $tempfile /db secedit.sdb /areas USER_RIGHTS /overwrite /quiet + } -ErrorAction SilentlyContinue + Write-Message -Level Verbose -Message "Removing secpol file on $computer" + Invoke-Command2 -Raw -ComputerName $computer -Credential $Credential -ScriptBlock { $temp = ([System.IO.Path]::GetTempPath()).TrimEnd(""); Remove-Item $temp\secpolByDbatools.cfg -Force > $NULL } + } + else { + Write-Message -Level Warning -Message "No SQL Service Accounts found on $Computer" + } + } + else { + Write-Message -Level Warning -Message "Failed to connect to $Computer" + } + } + catch { + Stop-Function -Continue -Message "Failure" -ErrorRecord $_ -Target $computer -Continue + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaQueryStoreConfig.ps1 b/functions/Set-DbaQueryStoreConfig.ps1 deleted file mode 100644 index 35a275a60a3e..000000000000 --- a/functions/Set-DbaQueryStoreConfig.ps1 +++ /dev/null @@ -1,262 +0,0 @@ -Function Set-DbaQueryStoreConfig -{ -<# -.SYNOPSIS -Configure Query Store settings for a specific or multiple databases. - -.DESCRIPTION -Configure Query Store settings for a specific or multiple databases. - -.PARAMETER SqlInstance -The SQL Server that you're connecting to. - -.PARAMETER SqlCredential -SqlCredential object used to connect to the SQL Server as a different user. - -.PARAMETER Databases -Set Query Store configuration for specific databases. - -.PARAMETER Exclude -Set Query Store configuration for all databases on the connected server except databases entered through this parameter. - -.PARAMETER AllDatabases -Run command against all user databases - -.PARAMETER State -Set the state of the Query Store. Valid options are "ReadWrite", "ReadOnly" and "Off". - -.PARAMETER FlushInterval -Set the flush to disk interval of the Query Store in seconds. - -.PARAMETER CollectionInterval -Set the runtime statistics collection interval of the Query Store in minutes. - -.PARAMETER MaxSize -Set the maximum size of the Query Store in MB. - -.PARAMETER CaptureMode -Set the query capture mode of the Query Store. Valid options are "Auto" and "All". - -.PARAMETER CleanupMode -Set the query cleanup mode policy. Valid options are "Auto" and "Off". - -.PARAMETER StaleQueryThreshold -Set the stale query threshold in days. - -.PARAMETER WhatIf -Shows what would happen if the command were to run - -.PARAMETER Confirm -Prompts for confirmation of every step. For example: - -Are you sure you want to perform this action? -Performing the operation "Changing Desired State" on target "pubs on SQL2016\VNEXT". -[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): - -.NOTES -Author: Enrico van de Laar ( @evdlaar ) -Tags: QueryStore -dbatools PowerShell module (https://dbatools.io) -Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. - -.LINK -https://dbatools.io/Set-QueryStoreConfig - -.EXAMPLE -Set-DbaQueryStoreConfig -SqlInstance ServerA\SQL -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode All -CleanupMode Auto -StaleQueryThreshold 100 -AllDatabases - -Configure the Query Store settings for all user databases in the ServerA\SQL Instance. - -.EXAMPLE -Set-DbaQueryStoreConfig -SqlInstance ServerA\SQL -FlushInterval 600 - -Only configure the FlushInterval setting for all Query Store databases in the ServerA\SQL Instance. - -.EXAMPLE -Set-DbaQueryStoreConfig -SqlInstance ServerA\SQL -Databases AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 - -Configure the Query Store settings for the AdventureWorks database in the ServerA\SQL Instance. - -.EXAMPLE -Set-DbaQueryStoreConfig -SqlInstance ServerA\SQL -Exclude AdventureWorks -State ReadWrite -FlushInterval 600 -CollectionInterval 10 -MaxSize 100 -CaptureMode all -CleanupMode Auto -StaleQueryThreshold 100 - -Configure the Query Store settings for all user databases except the AdventureWorks database in the ServerA\SQL Instance. - -#> - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("ServerInstance", "SqlServer")] - [object[]]$SqlInstance, - [PsCredential]$SqlCredential, - [switch]$AllDatabases, - [ValidateSet('ReadWrite', 'ReadOnly', 'Off')] - [string[]]$State, - [int64]$FlushInterval, - [int64]$CollectionInterval, - [int64]$MaxSize, - [ValidateSet('Auto', 'All')] - [string[]]$CaptureMode, - [ValidateSet('Auto', 'Off')] - [string[]]$CleanupMode, - [int64]$StaleQueryThreshold - ) - - DynamicParam - { - if ($SqlInstance) - { - Get-ParamSqlDatabases -SqlServer $SqlInstance[0] -SqlCredential $SqlCredential - } - } - - BEGIN - { - # Convert from RuntimeDefinedParameter object to regular array - $databases = $psboundparameters.Databases - $exclude = $psboundparameters.Exclude - } - - PROCESS - { - if (!$databases -and !$exclude -and !$alldatabases) - { - Write-Warning "You must specify databases to execute against using either -Databases, -Exclude or -AllDatabases" - continue - } - - if (!$State -and !$FlushInterval -and !$CollectionInterval -and !$MaxSize -and !$CaptureMode -and !$CleanupMode -and !$StaleQueryThreshold) - { - Write-Warning "You must specify something to change." - return - } - - foreach ($instance in $SqlInstance) - { - Write-Verbose "Connecting to $instance" - try - { - $server = Connect-SqlServer -SqlServer $instance -SqlCredential $SqlCredential - - } - catch - { - Write-Warning "Can't connect to $instance. Moving on." - continue - } - - if ($server.VersionMajor -lt 13) - { - - Write-Warning "The SQL Server Instance ($instance) has a lower SQL Server version than SQL Server 2016. Skipping server." - continue - } - - # We have to exclude all the system databases since they cannot have the Query Store feature enabled - $dbs = $server.Databases | Where-Object { $_.IsSystemObject -eq $false } - - if ($databases.count -gt 0) - { - $dbs = $dbs | Where-Object { $databases -contains $_.Name } - } - - if ($exclude.count -gt 0) - { - $dbs = $dbs | Where-Object { $exclude -notcontains $_.Name } - } - - - foreach ($db in $dbs) - { - Write-Verbose "Processing $db on $instance" - - if ($db.IsAccessible -eq $false) - { - Write-Warning "The database $db on server $instance is not accessible. Skipping database." - Continue - } - - if ($State) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DesiredState to $state")) - { - $db.QueryStoreOptions.DesiredState = $State - } - } - - if ($FlushInterval) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing DataFlushIntervalInSeconds to $FlushInterval")) - { - $db.QueryStoreOptions.DataFlushIntervalInSeconds = $FlushInterval - } - } - - if ($CollectionInterval) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StatisticsCollectionIntervalInMinutes to $CollectionInterval")) - { - $db.QueryStoreOptions.StatisticsCollectionIntervalInMinutes = $CollectionInterval - } - } - - if ($MaxSize) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing MaxStorageSizeInMB to $MaxSize")) - { - $db.QueryStoreOptions.MaxStorageSizeInMB = $MaxSize - } - } - - if ($CaptureMode) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing QueryCaptureMode to $CaptureMode")) - { - $db.QueryStoreOptions.QueryCaptureMode = $CaptureMode - } - } - - if ($CleanupMode) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing SizeBasedCleanupMode to $CleanupMode")) - { - $db.QueryStoreOptions.SizeBasedCleanupMode = $CleanupMode - } - } - - if ($StaleQueryThreshold) - { - if ($Pscmdlet.ShouldProcess("$db on $instance", "Changing StaleQueryThresholdInDays to $StaleQueryThreshold")) - { - $db.QueryStoreOptions.StaleQueryThresholdInDays = $StaleQueryThreshold - } - } - - # Alter the Query Store Configuration - if ($Pscmdlet.ShouldProcess("$db on $instance", "Altering Query Store configuration on database")) - { - try - { - $db.QueryStoreOptions.Alter() - $db.Refresh() - } - catch - { - Write-Warning "Could not modify configuration. Error was: $_" - continue - } - } - - - if ($Pscmdlet.ShouldProcess("$db on $instance", "Getting results from Get-DbaQueryStoreConfig")) - { - # Display resulting changes - Get-DbaQueryStoreConfig -SqlInstance $server -Databases $db.name -Verbose:$false - } - } - } - } -} diff --git a/functions/Set-DbaSpConfigure.ps1 b/functions/Set-DbaSpConfigure.ps1 new file mode 100644 index 000000000000..e41a5e7da25b --- /dev/null +++ b/functions/Set-DbaSpConfigure.ps1 @@ -0,0 +1,130 @@ +function Set-DbaSpConfigure { + <# + .SYNOPSIS + Changes the server level system configuration (sys.configuration/sp_configure) value for a given configuration + + .DESCRIPTION + This function changes the configured value for sp_configure settings. If the setting is dynamic this setting will be used, otherwise the user will be warned that a restart of SQL is required. + This is designed to be safe and will not allow for configurations to be set outside of the defined configuration min and max values. + While it is possible to set below the min, or above the max this can cause serious problems with SQL Server (including startup failures), and so is not permitted. + + .PARAMETER SqlInstance + SQL Server name or SMO object representing the SQL Server to connect to. This can be a + collection and receive pipeline input + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Name + The name of the configuration to be set -- Configs is auto-populated for tabbing convenience. + + .PARAMETER Value + The new value for the configuration + + .PARAMETER InputObject + Piped objectgs from Get-DbaSpConfigure + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: SpConfigure + Author: Nic Cain, https://sirsql.net/ + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaSpConfigure + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name ScanForStartupProcedures -Value 1 + + Adjusts the Scan for startup stored procedures configuration value to 1 and notifies the user that this requires a SQL restart to take effect + + .EXAMPLE + Get-DbaSpConfigure -SqlInstance sql2017, sql2014 -Name XPCmdShellEnabled, IsSqlClrEnabled | Set-DbaSpConfigure -Value $false + Sets the values for XPCmdShellEnabled and IsSqlClrEnabled on sql2017 and sql2014 to False + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name XPCmdShellEnabled -Value 1 + + Adjusts the xp_cmdshell configuration value to 1. + + .EXAMPLE + Set-DbaSpConfigure -SqlInstance localhost -Name XPCmdShellEnabled -Value 1 -WhatIf + + Returns information on the action that would be performed. No actual change will be made. + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [System.Management.Automation.PSCredential]$SqlCredential, + [Alias("NewValue", "NewConfig")] + [int]$Value, + [Alias("Config", "ConfigName")] + [string[]]$Name, + [parameter(ValueFromPipeline)] + [object[]]$InputObject, + [switch][Alias('Silent')] + $EnableException + ) + process { + foreach ($instance in $SqlInstance) { + $InputObject += Get-DbaSpConfigure -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Name $Name + } + + foreach ($configobject in $InputObject) { + $server = $InputObject.Parent + $currentRunValue = $configobject.RunningValue + $minValue = $configobject.MinValue + $maxValue = $configobject.MaxValue + $isDynamic = $configobject.IsDynamic + $configuration = $configobject.Name + + #Let us not waste energy setting the value to itself + if ($currentRunValue -eq $value) { + Stop-Function -Message "Value to set is the same as the existing value. No work being performed." -Continue -Target $server -Category InvalidData + } + + #Going outside the min/max boundary can be done, but it can break SQL, so I don't think allowing that is wise at this juncture + if ($value -lt $minValue -or $value -gt $maxValue) { + Stop-Function -Message "Value out of range for $configuration ($minValue <-> $maxValue)" -Continue -Category InvalidArgument + } + + If ($Pscmdlet.ShouldProcess($SqlInstance, "Adjusting server configuration $configuration from $currentRunValue to $value.")) { + try { + $server.Configuration.$configuration.ConfigValue = $value + $server.Configuration.Alter() + + [pscustomobject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + ConfigName = $configuration + OldValue = $currentRunValue + NewValue = $value + } + + #If it's a dynamic setting we're all clear, otherwise let the user know that SQL needs to be restarted for the change to take + if ($isDynamic -eq $false) { + Write-Message -Level Warning -Message "Configuration setting $configuration has been set, but restart of SQL Server is required for the new value `"$value`" to be used (old value: `"$currentRunValue`")" -Target $Instance + } + } + catch { + Stop-Function -Message "Unable to change config setting" -Target $Instance -ErrorRecord $_ -Continue -ContinueLabel main + } + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaSpn.ps1 b/functions/Set-DbaSpn.ps1 index 27ea921a7f39..a1d83198d727 100644 --- a/functions/Set-DbaSpn.ps1 +++ b/functions/Set-DbaSpn.ps1 @@ -1,12 +1,12 @@ -Function Set-DbaSpn -{ -<# +#ValidationTags#FlowControl,Pipeline# +function Set-DbaSpn { + <# .SYNOPSIS -Sets an SPN for a given service account in active directory, and also enables delegation to the same SPN +Sets an SPN for a given service account in active directory (and also enables delegation to the same SPN by default) .DESCRIPTION This function will connect to Active Directory and search for an account. If the account is found, it will attempt to add an SPN. Once the SPN -is added, the function will also set delegation to that service. In order to run this function, the credential you provide must have write +is added, the function will also set delegation to that service, unless -NoDelegation is specified. In order to run this function, the credential you provide must have write access to Active Directory. Note: This function supports -WhatIf @@ -17,27 +17,30 @@ The SPN you want to add .PARAMETER ServiceAccount The account you want the SPN added to -.PARAMETER DomainName -To set the SPN for another domain - .PARAMETER Credential The credential you want to use to connect to Active Directory to make the changes +.PARAMETER NoDelegation +Skips setting the delegation + +.PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + .PARAMETER Confirm Turns confirmations before changes on or off - + .PARAMETER WhatIf -Shows what would happen if the command was executed - +Shows what would happen if the command was executed + .NOTES Tags: SPN Author: Drew Furgiuele (@pittfurg), http://www.port1433.com dbatools PowerShell module (https://dbatools.io) Copyright (C) 2016 Chrissy LeMaire -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. +License: MIT https://opensource.org/licenses/MIT .LINK https://dbatools.io/Set-DbaSpn @@ -47,144 +50,126 @@ Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\acco Connects to Active Directory and adds a provided SPN to the given account. +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -EnableException + +Connects to Active Directory and adds a provided SPN to the given account, suppressing all error messages and throw exceptions that can be caught instead + .EXAMPLE Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -Credential (Get-Credential) Connects to Active Directory and adds a provided SPN to the given account. Uses alternative account to connect to AD. +.EXAMPLE +Set-DbaSpn -SPN MSSQLSvc\SQLSERVERA.domain.something -ServiceAccount domain\account -NoDelegation + +Connects to Active Directory and adds a provided SPN to the given account, without the delegation. + +.EXAMPLE +Test-DbaSpn -ComputerName sql2016 | Where { $_.isSet -eq $false } | Set-DbaSpn + +Sets all missing SPNs for sql2016 + .EXAMPLE Test-DbaSpn -ComputerName sql2016 | Where { $_.isSet -eq $false } | Set-DbaSpn -WhatIf - -Connects to Active Directory and adds a provided SPN to the given account. Uses alternative account to connect to AD. +Displays what would happen trying to set all missing SPNs for sql2016 #> - [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] - param ( - [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] - [Alias("RequiredSPN")] - [string]$SPN, - [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] - [Alias("InstanceServiceAccount", "AccountName")] - [string]$ServiceAccount, - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] - [pscredential]$Credential, - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] - [string]$DomainName - ) - - process - { - $OGServiceAccount = $ServiceAccount - if ($serviceaccount -like "*\*") - { - Write-Debug "Account provided in in domain\user format, stripping out domain info..." - $serviceaccount = ($serviceaccount.split("\"))[1] - } - if ($serviceaccount -like "*@*") - { - Write-Debug "Account provided in in user@domain format, stripping out domain info..." - $serviceaccount = ($serviceaccount.split("@"))[0] - } - - if ($domainName) { - $root = "" - $splitDomainName = $domainName.split(".") - ForEach ($s in $splitDomainName) { - if ($root -ne "") { - $root += "," - } - $root += "DC=" + $s - } - } else { - $root = ([ADSI]"LDAP://RootDSE").defaultNamingContext - } - $adsearch = New-Object System.DirectoryServices.DirectorySearcher - - if ($Credential) - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root), $($Credential.UserName), $($Credential.GetNetworkCredential().password) - } - else - { - $domain = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList ("LDAP://" + $root) - } - - $adsearch.SearchRoot = $domain - $adsearch.Filter = $("(&(samAccountName={0}))" -f $serviceaccount) - Write-Verbose "Looking for account $serviceAccount..." - $result = $adsearch.FindOne() - - #did we find the server account? - - if ($result -eq $null) - { - Write-Warning "The account specified for the SPN ($serviceAccount) does not exist on the domain" - continue - } - else - { - # Cool! Add an SPN - - $adentry = $result.GetDirectoryEntry() - $delegate = $true - - if ($PSCmdlet.ShouldProcess("$spn", "Adding SPN to service account")) - { - try - { - $null = $adentry.Properties['serviceprincipalname'].Add($spn) - Write-Verbose "Added SPN $spn to samaccount $serviceaccount" - $status = "Successfully added SPN" - $adentry.CommitChanges() - $set = $true - } - catch - { - Write-Warning "Could not add SPN. Error returned was: $_" - $set = $false - $status = "Failed to add SPN" - $delegate = $false - } - - [pscustomobject]@{ - Name = $spn - ServiceAccount = $OGServiceAccount - Property = "servicePrincipalName" - IsSet = $set - Notes = $status - } - } - - if (!$delegate) { continue } - - # Don't forget delegation! - $adentry = $result.GetDirectoryEntry() - if ($PSCmdlet.ShouldProcess("$spn", "Adding constrained delegation to service account for SPN")) - { - try - { - $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Add($spn) - Write-Verbose "Added kerberos delegation to $spn for samaccount $serviceaccount" - $adentry.CommitChanges() - $set = $true - $status = "Successfully added constrained delegation" - } - catch - { - Write-Warning "Could not add delegation. Error returned was: $_" - $set = $false - $status = "Failed to add constrained delegation" - } - - [pscustomobject]@{ - Name = $spn - ServiceAccount = $OGServiceAccount - Property = "msDS-AllowedToDelegateTo" - IsSet = $set - Notes = $status - } - } - } - } + [cmdletbinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("RequiredSPN")] + [string]$SPN, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName)] + [Alias("InstanceServiceAccount", "AccountName")] + [string]$ServiceAccount, + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] + [PSCredential]$Credential, + [switch]$NoDelegation, + [Alias('Silent')] + [switch]$EnableException + ) + + process { + #did we find the server account? + Write-Message -Message "Looking for account $ServiceAccount..." -Level Verbose + $searchfor = 'User' + if ($ServiceAccount.EndsWith('$')) { + $searchfor = 'Computer' + } + try { + $Result = Get-DbaADObject -ADObject $ServiceAccount -Type $searchfor -Credential $Credential -EnableException + } + catch { + Stop-Function -Message "AD lookup failure. This may be because the domain cannot be resolved for the SQL Server service account ($ServiceAccount). $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + if ($Result.Count -gt 0) { + try { + $adentry = $Result.GetUnderlyingObject() + } + catch { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has been found, but you don't have enough permission to inspect its properties $($_.Exception.Message)" -EnableException $EnableException -InnerErrorRecord $_ -Target $ServiceAccount + } + } + else { + Stop-Function -Message "The SQL Service account ($ServiceAccount) has not been found" -EnableException $EnableException -Target $ServiceAccount + } + # Cool! Add an SPN + $delegate = $true + if ($PSCmdlet.ShouldProcess("$spn", "Adding SPN to service account")) { + try { + $null = $adentry.Properties['serviceprincipalname'].Add($spn) + $status = "Successfully added SPN" + $adentry.CommitChanges() + Write-Message -Message "Added SPN $spn to $ServiceAccount" -Level Verbose + $set = $true + } + catch { + Write-Message -Message "Could not add SPN. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $false + $status = "Failed to add SPN" + $delegate = $false + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "servicePrincipalName" + IsSet = $set + Notes = $status + } + } + + #if we have the SPN set, we can add the delegation + if ($delegate) { + # but only if $NoDelegation is not passed + if (!$NoDelegation) { + if ($PSCmdlet.ShouldProcess("$spn", "Adding constrained delegation to service account for SPN")) { + try { + $null = $adentry.Properties['msDS-AllowedToDelegateTo'].Add($spn) + $adentry.CommitChanges() + Write-Message -Message "Added kerberos delegation to $spn for $ServiceAccount" -Level Verbose + $set = $true + $status = "Successfully added constrained delegation" + } + catch { + Write-Message -Message "Could not add delegation. $($_.Exception.Message)" -Level Warning -EnableException $EnableException -ErrorRecord $_ -Target $ServiceAccount + $set = $false + $status = "Failed to add constrained delegation" + } + + [pscustomobject]@{ + Name = $spn + ServiceAccount = $ServiceAccount + Property = "msDS-AllowedToDelegateTo" + IsSet = $set + Notes = $status + } + } + } + else { + Write-Message -Message "Skipping delegation as instructed" -Level Verbose + } + } + } } diff --git a/functions/Set-DbaStartupParameter.ps1 b/functions/Set-DbaStartupParameter.ps1 new file mode 100644 index 000000000000..e28b2046a0bc --- /dev/null +++ b/functions/Set-DbaStartupParameter.ps1 @@ -0,0 +1,447 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaStartupParameter { + <# + .SYNOPSIS + Sets the Startup Parameters for a SQL Server instance + + .DESCRIPTION + Modifies the startup parameters for a specified SQL Server Instance + + For full details of what each parameter does, please refer to this MSDN article - https://msdn.microsoft.com/en-us/library/ms190737(v=sql.105).aspx + + .PARAMETER SqlInstance + The SQL Server instance to be modified + + If the Sql Instance is offline path parameters will be ignored as we cannot test the instance's access to the path. If you want to force this to work then please use the Force switch + + .PARAMETER SqlCredential + Windows or Sql Login Credential with permission to log into the SQL instance + + .PARAMETER Credential + Windows Credential with permission to log on to the server running the SQL instance + + .PARAMETER MasterData + Path to the data file for the Master database + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER MasterLog + Path to the log file for the Master database + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER ErrorLog + Path to the SQL Server error log file + + Will be ignored if SqlInstance is offline or the Offline switch is set. To override this behaviour use the Force switch. This is to ensure you understand the risk as we cannot validate the path if the instance is offline + + .PARAMETER TraceFlags + A comma separated list of TraceFlags to be applied at SQL Server startup + By default these will be appended to any existing trace flags set + + .PARAMETER CommandPromptStart + Shortens startup time when starting SQL Server from the command prompt. Typically, the SQL Server Database Engine starts as a service by calling the Service Control Manager. + Because the SQL Server Database Engine does not start as a service when starting from the command prompt + + .PARAMETER MinimalStart + Starts an instance of SQL Server with minimal configuration. This is useful if the setting of a configuration value (for example, over-committing memory) has + prevented the server from starting. Starting SQL Server in minimal configuration mode places SQL Server in single-user mode + + .PARAMETER MemoryToReserve + Specifies an integer number of megabytes (MB) of memory that SQL Server will leave available for memory allocations within the SQL Server process, + but outside the SQL Server memory pool. The memory outside of the memory pool is the area used by SQL Server for loading items such as extended procedure .dll files, + the OLE DB providers referenced by distributed queries, and automation objects referenced in Transact-SQL statements. The default is 256 MB. + + .PARAMETER SingleUser + Start Sql Server in single user mode + + .PARAMETER NoLoggingToWinEvents + Don't use Windows Application events log + + .PARAMETER StartAsNamedInstance + Allows you to start a named instance of SQL Server + + .PARAMETER DisableMonitoring + Disables the following monitoring features: + + SQL Server performance monitor counters + Keeping CPU time and cache-hit ratio statistics + Collecting information for the DBCC SQLPERF command + Collecting information for some dynamic management views + Many extended-events event points + + ** Warning *\* When you use the -x startup option, the information that is available for you to diagnose performance and functional problems with SQL Server is greatly reduced. + + .PARAMETER SingleUserDetails + The username for single user + + .PARAMETER IncreasedExtents + Increases the number of extents that are allocated for each file in a filegroup. + + .PARAMETER TraceFlagsOverride + Overrides the default behaviour and replaces any existing trace flags. If not trace flags specified will just remove existing ones + + .PARAMETER StartUpConfig + Pass in a previously saved SQL Instance startup config + using this parameter will set TraceFlagsOverride to true, so existing Trace Flags will be overridden + + .PARAMETER Offline + Setting this switch will try perform the requested actions without connect to the SQL Server Instance, this will speed things up if you know the Instance is offline. + + When working offline, path inputs (MasterData, MasterLog and ErrorLog) will be ignored, unless Force is specified + + .PARAMETER Force + By default we test the values passed in via MasterData, MasterLog, ErrorLog + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Service, Startup, Parameter, Configure + Author: Stuart Moore (@napalmgram), stuart-moore.com + + dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) + Copyright (C) 2016 Chrissy LeMaire + License: MIT https://opensource.org/licenses/MIT + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser + + Will configure the SQL Instance server1\instance1 to startup up in Single User mode at next startup + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -IncreasedExtents + + Will configure the SQL Instance sql2016 to IncreasedExtents = True (-E) + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -IncreasedExtents:$false -WhatIf + + Shows what would happen if you attempted to configure the SQL Instance sql2016 to IncreasedExtents = False (no -E) + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -TraceFlags 8032,8048 + + This will append Trace Flags 8032 and 8048 to the startup parameters + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -SingleUser:$false -TraceFlagsOverride + + This will remove all trace flags and set SinguleUser to false + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -TraceFlags 8032,8048 -TraceFlagsOverride + + This will set Trace Flags 8032 and 8048 to the startup parameters, removing any existing Trace Flags + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -SingleUser:$false -TraceFlagsOverride -Offline + + This will remove all trace flags and set SinguleUser to false from an offline instance + + .EXAMPLE + Set-DbaStartupParameter -SqlInstance sql2016 -ErrorLog c:\Sql\ -Offline + + This will attempt to change the ErrorLog path to c:\sql\. However, with the offline switch this will not happen. To force it, use the -Force switch like so: + + Set-DbaStartupParameter -SqlInstance sql2016 -ErrorLog c:\Sql\ -Offline -Force + + .EXAMPLE + $StartupConfig = Get-DbaStartupParameter -SqlInstance server1\instance1 + Set-DbaStartupParameter -SqlInstance server1\instance1 -SingleUser -NoLoggingToWinEvents + #Restart your SQL instance with the tool of choice + #Do Some work + Set-DbaStartupParameter -SqlInstance server1\instance1 -StartUpConfig $StartUpConfig + #Restart your SQL instance with the tool of choice and you're back to normal + + In this example we take a copy of the existing startup configuration of server1\instance1 + + We then change the startup parameters ahead of some work + + After the work has been completed, we can push the original startup parameters back to server1\instance1 and resume normal operation + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] + param ([parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [string]$MasterData, + [string]$MasterLog, + [string]$ErrorLog, + [string[]]$TraceFlags, + [switch]$CommandPromptStart, + [switch]$MinimalStart, + [int]$MemoryToReserve, + [switch]$SingleUser, + [string]$SingleUserDetails, + [switch]$NoLoggingToWinEvents, + [switch]$StartAsNamedInstance, + [switch]$DisableMonitoring, + [switch]$IncreasedExtents, + [switch]$TraceFlagsOverride, + [object]$StartUpConfig, + [switch]$Offline, + [switch]$Force, + [Alias('Silent')] + [switch]$EnableException + ) + process { + + if (-not $Offline) { + try { + Write-Message -Level VeryVerbose -Message "Connecting to $SqlInstance" -Target $SqlInstance + $server = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + catch { + Write-Message -Level Warning -Message "Failed to connect to $SqlInstance, will try to work with just WMI. Path options will be ignored unless Force was indicated" + $Server = $SqlInstance + $Offline = $true + } + } + else { + Write-Message -Level Verbose -Message "Offline switch set, proceeding with just WMI" + $Server = $SqlInstance + } + + #Get Current parameters: + $currentstartup = Get-DbaStartupParameter -SqlInstance $server -Credential $Credential + $originalparamstring = $currentstartup.ParameterString + + Write-Message -Level Output -Message "Original startup parameter string: $originalparamstring" + + if ('startUpconfig' -in $PsBoundParameters.keys) { + Write-Message -Level VeryVerbose -Message "StartupObject passed in" + $newstartup = $StartUpConfig + $TraceFlagsOverride = $true + } + else { + Write-Message -Level VeryVerbose -Message "Parameters passed in" + $newstartup = $currentstartup.PSObject.copy() + foreach ($param in ($PsBoundParameters.keys | Where-Object { $_ -in ($newstartup.PSObject.Properties.name) })) { + if ($PsBoundParameters.item($param) -ne $newstartup.$param) { + $newstartup.$param = $PsBoundParameters.item($param) + } + } + } + + if (!($currentstartup.SingleUser)) { + + if ($newstartup.Masterdata.length -gt 0) { + if ($Offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested MasterData path" + $ParameterString += "-d$($CurrentStartup.MasterData);" + + } + else { + if ($Force) { + $ParameterString += "-d$($newstartup.MasterData);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.MasterData -Parent)) { + $ParameterString += "-d$($newstartup.MasterData);" + } + else { + Stop-Function -Message "Specified folder for Master Data file is not reachable by instance $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "MasterData value must be provided" + return + } + + if ($newstartup.ErrorLog.length -gt 0) { + if ($Offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested ErrorLog path" + $ParameterString += "-e$($CurrentStartup.ErrorLog);" + } + else { + if ($Force) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.ErrorLog -Parent)) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + else { + Stop-Function -Message "Specified folder for ErrorLog file is not reachable by $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "ErrorLog value must be provided" + return + } + + if ($newstartup.MasterLog.Length -gt 0) { + if ($offline -and -not $Force) { + Write-Message -Level Warning -Message "Working offline, skipping untested MasterLog path" + $ParameterString += "-l$($CurrentStartup.MasterLog);" + } + else { + if ($Force) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + elseif (Test-DbaSqlPath -SqlInstance $server -SqlCredential $SqlCredential -Path (Split-Path $newstartup.MasterLog -Parent)) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + else { + Stop-Function -Message "Specified folder for Master Log file is not reachable by $SqlInstance" + return + } + } + } + else { + Stop-Function -Message "MasterLog value must be provided." + return + } + } + else { + + Write-Message -Level Verbose -Message "Sql instance is presently configured for single user, skipping path validation" + if ($newstartup.MasterData.Length -gt 0) { + $ParameterString += "-d$($newstartup.MasterData);" + } + else { + Stop-Function -Message "Must have a value for MasterData" + return + } + if ($newstartup.ErrorLog.Length -gt 0) { + $ParameterString += "-e$($newstartup.ErrorLog);" + } + else { + Stop-Function -Message "Must have a value for Errorlog" + return + } + if ($newstartup.MasterLog.Length -gt 0) { + $ParameterString += "-l$($newstartup.MasterLog);" + } + else { + Stop-Function -Message "Must have a value for MsterLog" + return + } + } + + if ($newstartup.CommandPromptStart) { + $ParameterString += "-c;" + } + if ($newstartup.MinimalStart) { + $ParameterString += "-f;" + } + if ($newstartup.MemoryToReserve -notin ($null, 0)) { + $ParameterString += "-g$($newstartup.MemoryToReserve)" + } + if ($newstartup.SingleUser) { + if ($SingleUserDetails.length -gt 0) { + if ($SingleUserDetails -match ' ') { + $SingleUserDetails = """$SingleUserDetails""" + } + $ParameterString += "-m$SingleUserDetails;" + } + else { + $ParameterString += "-m;" + } + } + if ($newstartup.NoLoggingToWinEvents) { + $ParameterString += "-n;" + } + If ($newstartup.StartAsNamedInstance) { + $ParameterString += "-s;" + } + if ($newstartup.DisableMonitoring) { + $ParameterString += "-x;" + } + if ($newstartup.IncreasedExtents) { + $ParameterString += "-E;" + } + if ($newstartup.TraceFlags -eq 'None') { + $newstartup.TraceFlags = '' + } + if ($TraceFlagsOverride -and 'TraceFlags' -in $PsBoundParameters.keys) { + if ($null -ne $TraceFlags -and '' -ne $TraceFlags) { + $newstartup.TraceFlags = $TraceFlags -join ',' + $ParameterString += (($TraceFlags.split(',') | ForEach-Object { "-T$_" }) -join ';') + ";" + } + } + else { + if ('TraceFlags' -in $PsBoundParameters.keys) { + if ($null -eq $TraceFlags) { $TraceFlags = '' } + $oldflags = @($currentstartup.TraceFlags) -split ',' | Where-Object { $_ -ne 'None' } + $newflags = $TraceFlags + $newstartup.TraceFlags = (@($oldFlags) + @($newflags) | Sort-Object -Unique) -join ',' + } + elseif ($TraceFlagsOverride) { + $newstartup.TraceFlags = '' + } + else { + $newstartup.TraceFlags = if ($currentstartup.TraceFlags -eq 'None') { } + else { $currentstartup.TraceFlags -join ',' } + } + If ($newstartup.TraceFlags.Length -ne 0) { + $ParameterString += (($newstartup.TraceFlags.split(',') | ForEach-Object { "-T$_" }) -join ';') + ";" + } + } + + $instance = $SqlInstance.ComputerName + $instancename = $SqlInstance.InstanceName + Write-Message -Level Verbose -Message "Attempting to connect to $instancename on $instance" + + if ($instancename.Length -eq 0) { $instancename = "MSSQLSERVER" } + + $displayname = "SQL Server ($instancename)" + + if ($originalparamstring -eq "$ParameterString" -or "$originalparamstring;" -eq "$ParameterString") { + Stop-Function -Message "New parameter string would be the same as the old parameter string. Nothing to do." -Target $ParameterString + return + } + + $Scriptblock = { + $instance = $args[0] + $displayname = $args[1] + $ParameterString = $args[2] + + $wmisvc = $wmi.Services | Where-Object { $_.DisplayName -eq $displayname } + $wmisvc.StartupParameters = $ParameterString + $wmisvc.Alter() + $wmisvc.Refresh() + if ($wmisvc.StartupParameters -eq $ParameterString) { + $true + } + else { + $false + } + } + + if ($pscmdlet.ShouldProcess("Setting Sql Server start parameters on $SqlInstance to $ParameterString")) { + try { + if ($Credential) { + $response = Invoke-ManagedComputerCommand -ComputerName $instance -Credential $Credential -ScriptBlock $Scriptblock -ArgumentList $instance, $displayname, $ParameterString -EnableException + $output = Get-DbaStartupParameter -SqlInstance $server -Credential $Credential -EnableException + Add-Member -Force -InputObject $output -MemberType NoteProperty -Name OriginalStartupParameters -Value $originalparamstring + } + else { + $response = Invoke-ManagedComputerCommand -ComputerName $instance -ScriptBlock $Scriptblock -ArgumentList $instance, $displayname, $ParameterString -EnableException + $output = Get-DbaStartupParameter -SqlInstance $server -EnableException + Add-Member -Force -InputObject $output -MemberType NoteProperty -Name OriginalStartupParameters -Value $originalparamstring + } + + $output + + Write-Message -Level Output -Message "Startup parameters changed on $SqlInstance. You must restart SQL Server for changes to take effect." + } + catch { + Stop-Function -Message "Startup parameters failed to change on $SqlInstance. " -Target $SqlInstance -ErrorRecord $_ + return + } + } + } +} diff --git a/functions/Set-DbaTcpPort.ps1 b/functions/Set-DbaTcpPort.ps1 new file mode 100644 index 000000000000..dc75805dbf32 --- /dev/null +++ b/functions/Set-DbaTcpPort.ps1 @@ -0,0 +1,164 @@ +#ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# +function Set-DbaTcpPort { + <# + .SYNOPSIS + Changes the TCP port used by the specified SQL Server. + + .DESCRIPTION + This function changes the TCP port used by the specified SQL Server. + + .PARAMETER SqlInstance + The SQL Server that you're connecting to. + + .PARAMETER SqlCredential + Credential object used to connect to the SQL Server instance as a different user + + .PARAMETER Credential + Credential object used to connect to the Windows server itself as a different user + + .PARAMETER Port + TCPPort that SQLService should listen on. + + .PARAMETER IpAddress + Wich IpAddress should the portchange , if omitted allip (0.0.0.0) will be changed with the new portnumber. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .PARAMETER WhatIf + Shows what would happen if the command were to run. No actions are actually performed. + + .PARAMETER Confirm + Prompts you for confirmation before executing any changing operations within the command. + + .NOTES + Tags: Service, Port, TCP, Configure + Author: Hansson7707@gmail.com, @H0s0n77 + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaTcpPort + + .EXAMPLE + Set-DbaTcpPort -SqlInstance SqlInstance2014a -Port 1433 + + Sets the port number 1433 for allips on the default instance on SqlInstance2014a + + .EXAMPLE + Set-DbaTcpPort -SqlInstance winserver\sqlexpress -IpAddress 192.168.1.22 -Port 1433 + + Sets the port number 1433 for IP 192.168.1.22 on the sqlexpress instance on winserver + + .EXAMPLE + Set-DbaTcpPort -SqlInstance 'SQLDB2014A' ,'SQLDB2016B' -port 1337 + + Sets the port number 1337 for ALLIP's on SqlInstance SQLDB2014A and SQLDB2016B + #> + [CmdletBinding(ConfirmImpact = "High")] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter[]]$SqlInstance, + [PSCredential]$SqlCredential, + [PSCredential]$Credential, + [parameter(Mandatory = $true)] + [ValidateRange(1, 65535)] + [int]$Port, + [IpAddress[]]$IpAddress, + [Alias('Silent')] + [switch]$EnableException + ) + + begin { + + if ($IpAddress.Length -eq 0) { + $IpAddress = '0.0.0.0' + } + else { + if ($SqlInstance.count -gt 1) { + Stop-Function -Message "-IpAddress switch cannot be used with a collection of serveraddresses" -Target $SqlInstance + return + } + } + } + + process { + if (Test-FunctionInterrupt) { return } + + foreach ($instance in $SqlInstance) { + + try { + Write-Message -Level Verbose -Message "Connecting to $instance" + $server = Connect-SqlInstance -SqlInstance $instance -SqlCredential $sqlcredential + } + catch { + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue + } + + $wmiinstancename = $server.ServiceName + + + if ($server.IsClustered) { + Write-Message -Level Verbose -Message "Instance is clustered fetching nodes..." + $clusternodes = (Get-DbaClusterNode -SqlInstance $server).ComputerName -join ", " + + Write-Message -Level Output -Message "$instance is a clustered instance, portchanges will be reflected on all nodes ($clusternodes) after a failover" + } + + $scriptblock = { + $instance = $args[0] + $wmiinstancename = $args[1] + $port = $args[2] + $IpAddress = $args[3] + $sqlinstanceName = $args[4] + + $wmi = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer $instance + $wmiinstance = $wmi.ServerInstances | Where-Object { $_.Name -eq $wmiinstancename } + $tcp = $wmiinstance.ServerProtocols | Where-Object { $_.DisplayName -eq 'TCP/IP' } + $IpAddress = $tcp.IpAddresses | where-object { $_.IpAddress -eq $IpAddress } + $tcpport = $IpAddress.IpAddressProperties | Where-Object { $_.Name -eq 'TcpPort' } + + $oldport = $tcpport.Value + try { + $tcpport.value = $port + $tcp.Alter() + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $wmiinstancename + SqlInstance = $sqlinstanceName + OldPortNumber = $oldport + PortNumber = $Port + Status = "Success" + } + } + catch { + [pscustomobject]@{ + ComputerName = $env:COMPUTERNAME + InstanceName = $wmiinstancename + SqlInstance = $sqlinstanceName + OldPortNumber = $oldport + PortNumber = $Port + Status = "Failed: $_" + } + } + } + + try { + $computerName = $instance.ComputerName + $resolved = Resolve-DbaNetworkName -ComputerName $computerName + + Write-Message -Level Verbose -Message "Writing TCPPort $port for $instance to $($resolved.FQDN)..." + Invoke-ManagedComputerCommand -ComputerName $resolved.FQDN -ScriptBlock $scriptblock -ArgumentList $Server.NetName, $wmiinstancename, $port, $IpAddress, $server.DomainInstanceName -Credential $Credential + + } + catch { + Invoke-ManagedComputerCommand -ComputerName $instance.ComputerName -ScriptBlock $scriptblock -ArgumentList $Server.NetName, $wmiinstancename, $port, $IpAddress, $server.DomainInstanceName -Credential $Credential + } + } + } +} \ No newline at end of file diff --git a/functions/Set-DbaTempDbConfiguration.ps1 b/functions/Set-DbaTempDbConfiguration.ps1 new file mode 100644 index 000000000000..cb1c995bdcb7 --- /dev/null +++ b/functions/Set-DbaTempDbConfiguration.ps1 @@ -0,0 +1,274 @@ +function Set-DbaTempDbConfiguration { + <# + .SYNOPSIS + Sets tempdb data and log files according to best practices. + + .DESCRIPTION + Calculates tempdb size and file configurations based on passed parameters, calculated values, and Microsoft best practices. User must declare SQL Server to be configured and total data file size as mandatory values. Function then calculates the number of data files based on logical cores on the target host and create evenly sized data files based on the total data size declared by the user, with a log file 25% of the total data file size. + + Other parameters can adjust the settings as the user desires (such as different file paths, number of data files, and log file size). No functions that shrink or delete data files are performed. If you wish to do this, you will need to resize tempdb so that it is "smaller" than what the function will size it to before running the function. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER DataFileCount + Specifies the number of data files to create. If this number is not specified, the number of logical cores of the host will be used. + + .PARAMETER DataFileSizeMB + Specifies the total data file size in megabytes. This is distributed across the total number of data files. + + .PARAMETER LogFileSizeMB + Specifies the log file size in megabytes. If not specified, this will be set to 25% of total data file size. + + .PARAMETER DataFileGrowthMB + Specifies the growth amount for the data file(s) in megabytes. The default is 512 MB. + + .PARAMETER LogFileGrowthMB + Specifies the growth amount for the log file in megabytes. The default is 512 MB. + + .PARAMETER DataPath + Specifies the filesystem path in which to create the tempdb data files. If not specified, current tempdb location will be used. + + .PARAMETER LogPath + Specifies the filesystem path in which to create the tempdb log file. If not specified, current tempdb location will be used. + + .PARAMETER OutputScriptOnly + If this switch is enabled, only the T-SQL script to change the tempdb configuration is created and output. + + .PARAMETER OutFile + Specifies the filesystem path into which the generated T-SQL script will be saved. + + .PARAMETER DisableGrowth + If this switch is enabled, the tempdb files will be configured to not grow. This overrides -DataFileGrowthMB and -LogFileGrowthMB. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER EnableException + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. + + .NOTES + Tags: Tempdb, Space, Configure, Configuration + Author: Michael Fal (@Mike_Fal), http://mikefal.net + + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Set-DbaTempDbConfiguration + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 + + Creates tempdb with a number of data files equal to the logical cores where each file is equal to 1000MB divided by the number of logical cores, with a log file of 250MB. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -DataFileCount 8 + + Creates tempdb with 8 data files, each one sized at 125MB, with a log file of 250MB. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -OutputScriptOnly + + Provides a SQL script output to configure tempdb according to the passed parameters. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -DisableGrowth + + Disables the growth for the data and log files. + + .EXAMPLE + Set-DbaTempDbConfiguration -SqlInstance localhost -DataFileSizeMB 1000 -OutputScriptOnly + + Returns the T-SQL script representing tempdb configuration. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [parameter(Mandatory = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [int]$DataFileCount, + [Parameter(Mandatory = $true)] + [int]$DataFileSizeMB, + [int]$LogFileSizeMB, + [int]$DataFileGrowthMB = 512, + [int]$LogFileGrowthMB = 512, + [string]$DataPath, + [string]$LogPath, + [string]$OutFile, + [switch]$OutputScriptOnly, + [switch]$DisableGrowth, + [Alias('Silent')] + [switch]$EnableException + ) + begin { + $sql = @() + Write-Message -Level Verbose -Message "Connecting to $SqlInstance" + $server = Connect-SqlInstance $sqlinstance -SqlCredential $SqlCredential + + if ($server.VersionMajor -lt 9) { + Stop-Function -Message "SQL Server 2000 is not supported" + return + } + } + + process { + + if (Test-FunctionInterrupt) { + return + } + + $cores = $server.Processors + if ($cores -gt 8) { + $cores = 8 + } + + #Set DataFileCount if not specified. If specified, check against best practices. + if (-not $DataFileCount) { + $DataFileCount = $cores + Write-Message -Message "Data file count set to number of cores: $DataFileCount" -Level Verbose + } + else { + if ($DataFileCount -gt $cores) { + Write-Message -Message "Data File Count of $DataFileCount exceeds the Logical Core Count of $cores. This is outside of best practices." -Level Warning + } + Write-Message -Message "Data file count set explicitly: $DataFileCount" -Level Verbose + } + + $DataFilesizeSingleMB = $([Math]::Floor($DataFileSizeMB / $DataFileCount)) + Write-Message -Message "Single data file size (MB): $DataFilesizeSingleMB." -Level Verbose + + if ($DataPath) { + if ((Test-DbaSqlPath -SqlInstance $server -Path $DataPath) -eq $false) { + Stop-Function -Message "$datapath is an invalid path." + return + } + } + else { + $Filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 1').Tables[0].Rows[0].FileName + $DataPath = Split-Path $Filepath + } + + Write-Message -Message "Using data path: $datapath." -Level Verbose + + if ($LogPath) { + if ((Test-DbaSqlPath -SqlInstance $server -Path $LogPath) -eq $false) { + Stop-Function -Message "$LogPath is an invalid path." + return + } + } + else { + $Filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 2').Tables[0].Rows[0].FileName + $LogPath = Split-Path $Filepath + } + Write-Message -Message "Using log path: $LogPath." -Level Verbose + + # Check if the file growth needs to be disabled + if ($DisableGrowth) { + $DataFileGrowthMB = 0 + $LogFileGrowthMB = 0 + } + + # Check current tempdb. Throw an error if current tempdb is larger than config. + $CurrentFileCount = $server.Databases['tempdb'].ExecuteWithResults('SELECT count(1) as FileCount FROM sys.database_files WHERE type=0').Tables[0].Rows[0].FileCount + $TooBigCount = $server.Databases['tempdb'].ExecuteWithResults("SELECT TOP 1 (size/128) as Size FROM sys.database_files WHERE size/128 > $DataFilesizeSingleMB AND type = 0").Tables[0].Rows[0].Size + + if ($CurrentFileCount -gt $DataFileCount) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb has a greater number of files ($CurrentFileCount) than the calculated configuration ($DataFileCount)." + return + } + + if ($TooBigCount) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb ($TooBigCount MB) is larger than the calculated individual file configuration ($DataFilesizeSingleMB MB)." + return + } + + $EqualCount = $server.Databases['tempdb'].ExecuteWithResults("SELECT count(1) as FileCount FROM sys.database_files WHERE size/128 = $DataFilesizeSingleMB AND type = 0").Tables[0].Rows[0].FileCount + + if ($EqualCount -gt 0) { + Stop-Function -Message "Current tempdb not suitable to be reconfigured. The current tempdb is the same size as the specified DataFileSizeMB." + return + } + + Write-Message -Message "tempdb configuration validated." -Level Verbose + + $DataFiles = $server.Databases['tempdb'].ExecuteWithResults("select f.name as Name, f.physical_name as FileName from sys.filegroups fg join sys.database_files f on fg.data_space_id = f.data_space_id where fg.name = 'PRIMARY' and f.type_desc = 'ROWS'").Tables[0]; + + #Checks passed, process reconfiguration + for ($i = 0; $i -lt $DataFileCount; $i++) { + $File = $DataFiles.Rows[$i] + if ($File) { + $Filename = Split-Path $File.FileName -Leaf + $LogicalName = $File.Name + $NewPath = "$datapath\$Filename" + $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$LogicalName,filename='$NewPath',size=$DataFilesizeSingleMB MB,filegrowth=$DataFileGrowthMB);" + } + else { + $NewName = "tempdev$i.ndf" + $NewPath = "$datapath\$NewName" + $sql += "ALTER DATABASE tempdb ADD FILE(name=tempdev$i,filename='$NewPath',size=$DataFilesizeSingleMB MB,filegrowth=$DataFileGrowthMB);" + } + } + + if (-not $LogFileSizeMB) { + $LogFileSizeMB = [Math]::Floor($DataFileSizeMB / 4) + } + + $logfile = $server.Databases['tempdb'].ExecuteWithResults("SELECT name, physical_name as FileName FROM sys.database_files WHERE file_id = 2").Tables[0].Rows[0]; + $Filename = Split-Path $logfile.FileName -Leaf + $LogicalName = $logfile.Name + $NewPath = "$LogPath\$Filename" + $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$LogicalName,filename='$NewPath',size=$LogFileSizeMB MB,filegrowth=$LogFileGrowthMB);" + + Write-Message -Message "SQL Statement to resize tempdb." -Level Verbose + Write-Message -Message ($sql -join "`n`n") -Level Verbose + + if ($OutputScriptOnly) { + return $sql + } + elseif ($OutFile) { + $sql | Set-Content -Path $OutFile + } + else { + if ($Pscmdlet.ShouldProcess($SqlInstance, "Executing query and informing that a restart is required.")) { + try { + $server.Databases['master'].ExecuteNonQuery($sql) + Write-Message -Level Verbose -Message "tempdb successfully reconfigured." + + [PSCustomObject]@{ + ComputerName = $server.NetName + InstanceName = $server.ServiceName + SqlInstance = $server.DomainInstanceName + DataFileCount = $DataFileCount + DataFileSizeMB = $DataFileSizeMB + SingleDataFileSizeMB = $DataFilesizeSingleMB + LogSizeMB = $LogFileSizeMB + DataPath = $DataPath + LogPath = $LogPath + DataFileGrowthMB = $DataFileGrowthMB + LogFileGrowthMB = $LogFileGrowthMB + } + + Write-Message -Level Output -Message "tempdb reconfigured. You must restart the SQL Service for settings to take effect." + } + catch { + Stop-Function -Message "Unable to reconfigure tempdb. Exception: $_" -Target $sql -InnerErrorRecord $_ + return + } + } + } + } + end { + Test-DbaDeprecation -DeprecatedOn "1.0.0" -EnableException:$false -Alias Set-SqlTempDbConfiguration + } +} diff --git a/functions/Set-SqlTempDBConfiguration.ps1 b/functions/Set-SqlTempDBConfiguration.ps1 deleted file mode 100644 index 6bbadd1e977f..000000000000 --- a/functions/Set-SqlTempDBConfiguration.ps1 +++ /dev/null @@ -1,271 +0,0 @@ -function Set-SqlTempDbConfiguration -{ -<# -.SYNOPSIS -Sets tempdb data and log files according to best practices. - -.DESCRIPTION -Function to calculate tempdb size and file configurations based on passed parameters, calculated values, and Microsoft -best practices. User must declare SQL Server to be configured and total data file size as mandatory values. Function will -then calculate number of data files based on logical cores on the target host and create evenly sized data files based -on the total data size declared by the user, with a log file 25% of the total data file size. Other parameters can adjust -the settings as the user desires (such as different file paths, number of data files, and log file size). The function will -not perform any functions that would shrink or delete data files. If a user desires this, they will need to reduce tempdb -so that it is "smaller" than what the function will size it to before runnint the function. - -.NOTES -Original Author: Michael Fal (@Mike_Fal), http://mikefal.net - -dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com) -Copyright (C) 2016 Chrissy LeMaire - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -.PARAMETER SqlServer -SQLServer name or SMO object representing the SQL Server to connect to - -.PARAMETER SqlCredential -PSCredential object to connect under. If not specified, currend Windows login will be used. - -.PARAMETER DataFileCount -Integer of number of datafiles to create. If not specified, function will use logical cores of host. - -.PARAMETER DataFileSizeMB -Total data file size in megabytes - -.PARAMETER LogFileSizeMB -Log file size in megabyes. If not specified, function will use 25% of total data file size. - -.PARAMETER DataPath -File path to create tempdb data files in. If not specified, current tempdb location will be used. - -.PARAMETER LogPath -File path to create tempdb log file in. If not specified, current tempdb location will be used. - -.PARAMETER Script -Switch to generate script for tempdb configuration. - -.PARAMETER OutFile -Path to file to save the generated script for tempdb configuration - -.PARAMETER WhatIf -Switch to generate configuration object. - -.PARAMETER Confirm -Prompts you for confirmation before executing any changing operations within the command. - -.LINK -https://dbatools.io/Set-SqltempdbConfiguration - -.EXAMPLE -Set-SqltempdbConfiguration -SqlServer localhost -DataFileSizeMB 1000 - -Creates tempdb with a number of datafiles equal to the logical cores where -each one is equal to 1000MB divided by number of logical cores and a log file -of 250MB - -.EXAMPLE -Set-SqltempdbConfiguration -SqlServer localhost -DataFileSizeMB 1000 -DataFileCount 8 - -Creates tempdb with a number of datafiles equal to the logical cores where -each one is equal to 125MB and a log file of 250MB - -.EXAMPLE -Set-SqltempdbConfiguration -SqlServer localhost -DataFileSizeMB 1000 -Script - -Provides a SQL script output to configure tempdb according to the passed parameters - -.EXAMPLE -Set-SqltempdbConfiguration -SqlServer localhost -DataFileSizeMB 1000 -Script - -Returns PSObject representing tempdb configuration. -#> - [CmdletBinding(SupportsShouldProcess = $true)] - param ( - [parameter(Mandatory = $true)] - [Alias("ServerInstance", "SqlInstance")] - [object]$SqlServer, - [System.Management.Automation.PSCredential]$SqlCredential, - [int]$DataFileCount, - [Parameter(Mandatory = $true)] - [int]$datafilesizemb, - [int]$LogFileSizeMB, - [string]$DataPath, - [string]$LogPath, - [string]$OutFile, - [switch]$Script - ) - BEGIN - { - $sql = @() - Write-Verbose "Connecting to $SqlServer" - $server = Connect-SqlServer $SqlServer -SqlCredential $SqlCredential - - if ($server.VersionMajor -lt 9) - { - throw "SQL Server 2000 is not supported" - } - } - - PROCESS - { - $cores = $server.Processors - if ($cores -gt 8) { $cores = 8 } - - #Set DataFileCount if not specified. If specified, check against best practices. - if (-not $datafilecount) - { - $datafilecount = $cores - Write-Verbose "Data file count set to number of cores: $datafilecount" - } - else - { - if ($datafilecount -gt $cores) - { - Write-Warning "Data File Count of $datafilecount exceeds the Logical Core Count of $cores. This is outside of best practices." - } - Write-Verbose "Data file count set explicitly: $datafilecount" - } - - $dataFilesizeSingleMB = $([Math]::Floor($datafilesizemb/$datafilecount)) - Write-Verbose "Single data file size (MB): $dataFilesizeSingleMB" - - if ($datapath) - { - if ((Test-SqlPath -SqlServer $server -Path $datapath) -eq $false) - { - throw "$datapath is an invalid path." - } - } - else - { - $filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 1').Tables[0].FileName - $datapath = Split-Path $filepath - } - - Write-Verbose "Using data path: $datapath" - - if ($logpath) - { - if ((Test-SqlPath -SqlServer $server -Path $logpath) -eq $false) - { - throw "$logpath is an invalid path." - } - } - else - { - $filepath = $server.Databases['tempdb'].ExecuteWithResults('SELECT physical_name as FileName FROM sys.database_files WHERE file_id = 2').Tables[0].FileName - $logpath = Split-Path $filepath - } - Write-Verbose "Using log path: $logpath" - - $LogSizeMBActual = if (-not $LogFileSizeMB) { $([Math]::Floor($datafilesizemb/4)) } - - $config = [PSCustomObject]@{ - SqlServer = $server.Name - DataFileCount = $datafilecount - DataFileSizeMB = $datafilesizemb - SingleDataFileSizeMB = $dataFilesizeSingleMB - LogSizeMB = $LogSizeMBActual - DataPath = $datapath - LogPath = $logpath - } - - # Check current tempdb. Throw an error if current tempdb is 'larger' than config. - $currentfilecount = $server.Databases['tempdb'].ExecuteWithResults('SELECT count(1) as FileCount FROM sys.database_files WHERE type=0').Tables[0].FileCount - $toobigcount = $server.Databases['tempdb'].ExecuteWithResults("SELECT count(1) as FileCount FROM sys.database_files WHERE size/128 > $dataFilesizeSingleMB AND type = 0").Tables[0].FileCount - - if ($currentfilecount -gt $datafilecount) - { - throw "Current tempdb not suitable to be reconfigured. The current tempdb has a greater number of files than the calculated configuration." - } - - if ($toobigcount -gt 0) - { - throw "Current tempdb not suitable to be reconfigured. The current tempdb is larger than the calculated configuration." - } - - $equalcount = $server.Databases['tempdb'].ExecuteWithResults("SELECT count(1) as FileCount FROM sys.database_files WHERE size/128 = $dataFilesizeSingleMB AND type = 0").Tables[0].FileCount - - if ($equalcount -gt 0) - { - throw "Current tempdb not suitable to be reconfigured. The current tempdb is the same size as the specified DataFileSizeMB." - } - - Write-Verbose "tempdb configuration validated." - - $datafiles = $server.Databases['tempdb'].ExecuteWithResults("select f.Name, f.physical_name as FileName from sys.filegroups fg join sys.database_files f on fg.data_space_id = fg.data_space_id where fg.name = 'PRIMARY' and f.type_desc = 'ROWS'").Tables[0] - - #Checks passed, process reconfiguration - for ($i = 0; $i -lt $datafilecount; $i++) - { - $file = $datafiles.Rows[$i] - if ($file) - { - $filename = Split-Path $file.FileName -Leaf - $logicalname = $file.Name - $newpath = "$datapath\$filename" - $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$logicalname,filename='$newpath',size=$dataFilesizeSingleMB MB,filegrowth=512MB);" - } - else - { - $newname = "tempdev$i.ndf" - $newpath = "$datapath\$newname" - $sql += "ALTER DATABASE tempdb ADD FILE(name=tempdev$i,filename='$newpath',size=$dataFilesizeSingleMB MB,filegrowth=512MB);" - } - } - - if (-not $LogFileSizeMB) - { - $LogFileSizeMB = [Math]::Floor($datafilesizemb/4) - } - - $logfile = $server.Databases['tempdb'].ExecuteWithResults("SELECT name, physical_name as FileName FROM sys.database_files WHERE file_id = 2").Tables[0] - $filename = Split-Path $logfile.FileName -Leaf - $logicalname = $logfile.Name - $newpath = "$logpath\$filename" - $sql += "ALTER DATABASE tempdb MODIFY FILE(name=$logicalname,filename='$newpath',size=$LogFileSizeMB MB,filegrowth=512MB);" - - Write-Verbose "SQL Statement to resize tempdb" - Write-Verbose ($sql -join "`n`n") - - if ($Script) - { - return $sql - } - elseif ($OutFile) - { - $sql | Set-Content -Path $OutFile - } - else - { - If ($Pscmdlet.ShouldProcess($SqlServer, "Executing $sql and informing that a restart is required.")) - { - try - { - $server.Databases['master'].ExecuteNonQuery($sql) - Write-Verbose "tempdb successfully reconfigured" - Write-Warning "tempdb reconfigured. You must restart the SQL Service for settings to take effect." - } - catch - { - # write-exception writes the full exception to file - Write-Exception $_ - throw "Unable to reconfigure tempdb" - } - } - } - } - - END - { - $server.ConnectionContext.Disconnect() - } -} diff --git a/functions/Show-DbaDatabaseList.ps1 b/functions/Show-DbaDatabaseList.ps1 new file mode 100644 index 000000000000..eb7672fb1080 --- /dev/null +++ b/functions/Show-DbaDatabaseList.ps1 @@ -0,0 +1,194 @@ +function Show-DbaDatabaseList { + <# + .SYNOPSIS + Shows a list of databases in a GUI. + + .DESCRIPTION + Shows a list of databases in a GUI. Returns a string holding the name of the selected database. Hitting cancel returns null. + + .PARAMETER SqlInstance + The SQL Server Instance to connect to.. + + .PARAMETER SqlCredential + Login to the target instance using alternative credentials. Windows and SQL Authentication supported. Accepts credential objects (Get-Credential) + + .PARAMETER Title + Title of the window being displayed. Default is "Select Database". + + .PARAMETER Header + Header text displayed above the database listing. Default is "Select the database:". + + .PARAMETER DefaultDb + Specify a database to have selected when the window appears. + + .NOTES + Tags: Database + Website: https://dbatools.io + Copyright: (C) Chrissy LeMaire, clemaire@gmail.com + License: MIT https://opensource.org/licenses/MIT + + .LINK + https://dbatools.io/Show-DbaDatabaseList + + .EXAMPLE + Show-DbaDatabaseList -SqlInstance sqlserver2014a + + Shows a GUI list of databases using Windows Authentication to connect to the SQL Server. Returns a string of the selected database. + + .EXAMPLE + Show-DbaDatabaseList -Source sqlserver2014a -SqlCredential $cred + + Shows a GUI list of databases using SQL credentials to connect to the SQL Server. Returns a string of the selected database. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias("ServerInstance", "SqlServer")] + [DbaInstanceParameter]$SqlInstance, + [PSCredential]$SqlCredential, + [string]$Title = "Select Database", + [string]$Header = "Select the database:", + [string]$DefaultDb + ) + + begin { + try { + Add-Type -AssemblyName PresentationFramework + } + catch { + throw "Windows Presentation Framework required but not installed" + } + + function Add-TreeItem { + Param ( + [string]$name, + [object]$parent, + [string]$tag + ) + + $childitem = New-Object System.Windows.Controls.TreeViewItem + $textblock = New-Object System.Windows.Controls.TextBlock + $textblock.Margin = "5,0" + $stackpanel = New-Object System.Windows.Controls.StackPanel + $stackpanel.Orientation = "Horizontal" + $image = New-Object System.Windows.Controls.Image + $image.Height = 20 + $image.Width = 20 + $image.Stretch = "Fill" + $image.Source = $dbicon + $textblock.Text = $name + $childitem.Tag = $name + + if ($name -eq $DefaultDb) { + $childitem.IsSelected = $true + $script:selected = $name + } + + [void]$stackpanel.Children.Add($image) + [void]$stackpanel.Children.Add($textblock) + + $childitem.Header = $stackpanel + [void]$parent.Items.Add($childitem) + } + + function Convert-b64toimg { + param ($base64) + + $bitmap = New-Object System.Windows.Media.Imaging.BitmapImage + $bitmap.BeginInit() + $bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64) + $bitmap.EndInit() + $bitmap.Freeze() + return $bitmap + } + + $dbicon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAFRSURBVDhPY/j//z9VMVZBSjCCgQZunFn6/8zenv+7llf83zA75/+6WTn/N80v+L93ddP/M/tnY2jAayDIoNvn5/5/cX/t/89vdv7/9fUQGIPYj2+t/H/xyJT/O1ZUoWjCaeCOxcX///48ShSeWhMC14jXwC9Xs/5/fzHr/6/PW+GaQS78/WH9/y+Pe8DyT3fYEmcgKJw+HHECawJp/vZ60f8v95v/fzgd8P/tVtn/L1cw/n+0iOH/7TlMxBkIigBiDewr9iVsICg2qWrg6qnpA2dgW5YrYQOX9icPAQPfU9PA2S2RRLuwMtaGOAOf73X+//FyGl4DL03jIM5AEFjdH/x//+Lo/1cOlP9/dnMq2MA3x/z/312l/P/4JNH/axoU/0/INUHRhNdAEDi+pQ1cZIFcDEpvoPCaVOTwf1Gjy/9ds5MxNGAYSC2MVZB8/J8BAGcHwqQBNWHRAAAAAElFTkSuQmCC" + $foldericon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAHaSURBVDhPY/j//z9VMVZBSjBWQUowVkFKMApnzZL+/+gYWZ4YDGeANL95sun/j3fbwPjbm5X/Pz+cRLKhcAayq2B45YKe/8vndoHx4lltYLxgajMKhumHYRQDf37Yh4J/fNry//fb1f9/v1n6/8/Tqf//3O/6/+dO9f9fV4v+fzmV/v/L0aj/lflJQO1YDAS5AmwI1MvfPyAZ9KgbYtDlvP/fzyT9/3w45P+HPT7/z8+UwG0gyDvIBmIYBnQVyDCQq0CGPV9p8v94P/f/rKQwoHYsBs4HhgfIQJjLfr+YjdOwt5tt/z9eov1/fxf3/+ggD6B2HAaCXQYKM6hhv+81oYQXzLCXq03/P5qn/H9LE/9/LycroHYsBs7oq4EYCDIM6FVshr3Z4gg2DOS6O9Nk/q+sFvlvZawD1I7FwKldleC0h2zY9wuZEMP2+aMYdn+W/P/rE0T/zy+T+q+jJg/UjsXASe1l/z/cX/T/1dn8/492ePy/vc7s/82VOv8vLVT9f3yGwv89ffL/1zXL/l9dJwF2GciwaYVy/xVlxIDasRjY31Lyv7Uy+39ZTvz/1JiA/8Hejv8dLA3+62sqgTWJC/HixDAzQBjOoBbGKkgJxipICcYqSD7+zwAAkIiWzSGuSg0AAAAASUVORK5CYII=" + $dbatoolsicon = Convert-b64toimg "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAO9SURBVEhL3VVdTFNXHO9MzPTF+OzDeBixFdTMINIWsAUK3AIVkFvAIQVFRLYZKR8Wi1IEKV9DYB8PGFAyEx8QScySabYY5+I2JvK18iWISKGk0JGhLzA3+e2c29uHtpcvH/0lv9yennN+v3vO/3fOFb2fCAg4vXWPNOmMRJ745TtTSskqeElviGXJ0XtkWvjJkyGLPoFAVQZoe/NkX/n6Mh/ysu4Qy7WZdJAutxRW6zT6LcNQaE4LiGgREH4cibpCMNqzCIk9hbScEoSSZ0zKOa7fRxG/k5d1h8ukvO4a5ubmMT1jw5E0vZcBZWzqOTS3dcB8tRXZeRX4/v5DZH5uIu0Wrn8NEzaNDjgYoUPd120oMjViX2iql8H6ZFd8DzE7eFl3iOWpuyQydlh44kbJroilSd8RuQ+cqh7wC9Z+JJaxY8KTN0gp+5Yk9DaREzYhb5FOBwZFZ6LlZifKa5ux//AxYTHCvSEp8A9O5n77B6dwqXS119guZ+GrGq9jfn4eM7ZZxB/PdxN2UfOpHq3kRWq/uoE8Yx3u/fQLzhSYUdN0g+tfN126z0oxNj6BJz0Dq0b4E2UawuJzuPhKyZmKYr/AocgMrk37VzWRBLGRdE/psuXqk9wkT/GNUCJLWqS3By/rDh9FxjaSrnahiZ7cq8wCUzKImLIJqC+Ngbk4gmjjIKKKB6Aq7l+OLBmfVF0YnlQZR1p4eSd2y5IiyEr+oyJ0CwIi0gUNKAOPmnG04Q0utf+DHweWkFjjQOyVWajLpsCUPkeUcRgqAzE09Dfz8k64aqI9YcDziUk87bMgOCZL0CQ0ux2J9UtIbXyFwall/PD0NeLKrU6DkhGymj8RXtRDjU7x8k64TKpJQmi6bLOzSEgv8DYhNWMujiK+9jU0VQs4Vm/H2MwSOh4vcP+rii2cQVh+F+IqbRJe3glyReuoSFBUJtpu3eWulv2h3ueE1iOu0g5N9QL3jLk8jerbdrz59y1yGoYQUdSLsII/CLscIsD9UPrLUz4myXhBhWjCPMVdPBBnhMbsIAZzSDDbcOvRIhyLy6i4+Qyq82QFxECR9xjK/K5OXtodNHo+CsW2tagunbxADbK+sXP16Bv/G7lNQ8hpHEX21UGoDb/j8NmfoSzoNvCymwdTPvMotsKGB32LaL1H0mS0oOHOFLpH/0L3iAOF3/YSk4dgTBMh/JTNgdVbtzNl1il12UuSpHE+SRayTb0IL3yCMP2vUJKtUuh/szNNK8Jfxw3BZNpiMoGjiKPJm54Ffw8gEv0PQRYX7wDAUKEAAAAASUVORK5CYII=" + + $sourceserver = Connect-SqlInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential + } + + process { + # Create XAML form in Visual Studio, ensuring the ListView looks chromeless + [xml]$xaml = " + + +