面向系统管理员的-powershell-指南-全-

面向系统管理员的 powershell 指南(全)

原文:zh.annas-archive.org/md5/2805a6cdf89e472bdd9e66f20b3a73a3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

图片

在我的 IT 职业生涯中,我做过各种不同的工作:我曾在技术支持岗位接听电话,作为技术员拜访用户并告诉他们重启电脑,作为系统管理员维护服务器,作为系统工程师设计和构建解决方案,并作为网络工程师学习 OSPF 和 RIP 路由的区别。

直到我发现 PowerShell,我才意识到我能对某项技术如此热情。PowerShell 在多方面改变了我的生活,它是改变我职业轨迹最深远的技术。这门语言帮助我成为了公司中的关键人才,因为我知道如何为团队节省无数工时,它也为我带来了我的第一个六位数薪资。PowerShell 实在太酷了,以至于我决定要与全世界分享它,从那时起,我连续五年获得了微软 MVP 奖项。

在本书中,我将向你展示如何使用 PowerShell 来自动化成千上万的任务,构建定制工具而不是购买现成的产品,并将各种工具连接在一起。你可能不想成为 PowerShell 社区的活跃成员,但我可以保证,学习 PowerShell 会让你掌握许多企业需要并积极寻求的技能。

为什么选择 PowerShell?

曾被称为 Monad(参见 www.jsnover.com/Docs/MonadManifesto.pdf),并在 2003 年作为比 VBScript 更直观的自动化任务方式提出的 Microsoft PowerShell 是一门通用的自动化、脚本和开发语言。PowerShell 的诞生旨在弥合脚本、自动化和运维人员之间的鸿沟。它旨在使用户能够通过脚本自动化任务,而无需学习计算机编程。这使得它特别适用于没有软件开发背景的系统管理员。如果你是一个时间紧张的系统管理员,PowerShell 是你值得拥有的好帮手。

PowerShell 现在已成为一个开源、无处不在、跨平台的脚本和开发语言。你可以使用 PowerShell 不仅来配置完整的服务器集群,还可以创建文本文件或设置注册表项。由于 IT 专业人员、开发者、DevOps 工程师、数据库管理员和系统工程师的广泛使用,成千上万的软件产品和服务现在都支持 PowerShell。

本书适用对象

本书面向那些厌倦了在同一个图形界面中重复点击、每年做着同样任务超过 500 次的 IT 专业人员和系统管理员。它同样适用于那些在自动化新服务器环境、进行自动化测试或自动化整个持续集成/持续交付(CI/CD)构建管道中遇到困难的 DevOps 工程师。

没有单一的人群能从 PowerShell 中获得最大的好处。PowerShell 用户的传统工作角色是“Windows 商店”中的微软系统管理员,但 PowerShell 通常适用于任何 IT 运维人员的工具包。如果你从事 IT 工作,并且不认为自己是开发人员,那么这本书适合你。

本书简介

在本书中,我将通过实际操作来教学,使用大量示例和真实世界的用例。与其 告诉 你什么是变量,我会 展示 给你看。如果你正在寻找一本传统的教科书,这本书不适合你。

我不会将 PowerShell 拆分成各个部分并独立介绍每个功能,因为这不是你在现实世界中使用 PowerShell 的方式。例如,我不会期待你知道 函数for 循环 的书面定义,而是尽可能将功能结合起来,帮助你全面理解眼前的问题以及如何解决它。

本书分为三部分。第一部分:基础知识 为 PowerShell 新手提供了他们所需的知识,帮助他们与经验丰富的老手并驾齐驱。如果你已经具备中级或更高级别的 PowerShell 技能,可以跳过 第八章。

第一章–第七章 涵盖了 PowerShell 语言本身。你将学习基础知识,包括如何查找帮助和发现新命令,以及一些其他编程语言中常见的编程概念,如变量、对象、函数、模块和错误处理基础。

第八章 解释了如何使用 PowerShell 远程连接并在远程计算机上运行命令。

第九章 介绍了流行的 PowerShell 测试框架 Pester,你将在本书中使用它。

第二部分:自动化日常任务 中,你将应用在 第一部分 中学到的知识,开始自动化常见任务。

第十章–第十三章 涵盖了如何解析结构化数据以及许多 IT 管理员常用的领域,如 Active Directory、Azure 和亚马逊 Web 服务(AWS)。

第十四章 向你展示如何构建一个工具,帮助你在自己的环境中进行服务器清单管理。

第三部分:构建你自己的模块 中,你将专注于构建一个名为 PowerLab 的单一 PowerShell 模块,以展示 PowerShell 的潜力。我们将涵盖良好的模块设计和函数的最佳实践。即使你认为自己是一个高级 PowerShell 脚本编写者,也一定能从 第三部分 中学到一些东西。

第十五章–第二十章 解释了如何使用 PowerShell 自动化整个实验室或测试服务器环境,演示了如何配置 Hyper-V 虚拟机、安装操作系统、以及部署和配置 IIS 和 SQL 服务器。

我希望本书能帮助你入门 PowerShell。如果你是初学者,我希望它能给你勇气开始自动化;如果你是经验丰富的脚本编写者,我希望它能向你展示一些你可能不太熟悉的技巧。

让我们开始编写脚本吧!

第一部分

基础知识

正如老话所说,学会爬行才可以走路。在 PowerShell 中构建工具也是如此。在本书的第二部分和第三部分中,你将学习如何构建一些强大的工具。但在此之前,你需要先掌握语言的基础。如果你已经是一个中级或高级的 PowerShell 用户,可以跳过第一部分。虽然你可能会发现一两个之前不知道的小知识,但可能不值得花时间去消化这一整部分内容。

但是,如果你是 PowerShell 新手,那么这一部分就是为你准备的。我们将探索 PowerShell 语言,了解一些你将常常使用的结构。我们将涵盖从基础编程概念,如变量和函数,到编写脚本、远程运行脚本以及使用名为 Pester 的工具进行测试的内容。因为我们只会讲解基础知识,所以暂时不会构建太多工具——这就是第二部分和第三部分的内容。这里我们将通过小例子来帮助你掌握这门语言。你将首次了解到 PowerShell 的强大功能。让我们开始吧!

第一章:入门

图片

PowerShell 这个名称指代两件事。其一是命令行外壳,默认安装在所有最新版本的 Windows 上(从 Windows 7 开始),并且最近通过 PowerShell Core 可在 Linux 和 macOS 操作系统上使用。其二是脚本语言。二者合起来指的是一个框架,可以用来自动化一切任务,从一次重启 100 台服务器,到构建一个完整的自动化系统,控制你整个数据中心的运作。

在本书的前几章中,你将使用 PowerShell 控制台来熟悉 PowerShell 的基础知识。一旦掌握了基础,你将进入更高级的主题,包括编写脚本、函数和自定义模块。

本章介绍基础内容:一些基本命令,以及如何查找和阅读帮助页面。

打开 PowerShell 控制台

本书中的示例使用的是 PowerShell v5.1,这是 Windows 10 自带的版本。较新的 PowerShell 版本拥有更多功能和 bug 修复,但自版本 2 起,PowerShell 的基本语法和核心功能并未发生剧烈变化。

在 Windows 10 中打开 PowerShell,输入PowerShell在开始菜单中搜索。你应该会看到 Windows PowerShell 选项出现在中间,点击该选项即可打开一个蓝色控制台和闪烁的光标,如图 1-1 所示。

图片

图 1-1:PowerShell 控制台

闪烁的光标表示 PowerShell 已准备好接受你的输入。请注意,你的提示符——以PS>开头的那一行——可能与我的不同;提示符中的文件路径表示你在系统中的当前位置。如你在我控制台的标题中所见,我右键点击了 PowerShell 图标,并以管理员身份运行它。这为我提供了完全的权限,并将我启动在C:\Windows\system32\WindowsPowerShell\v1.0目录中。

使用 DOS 命令

一旦打开 PowerShell,你就可以开始探索了。如果你以前使用过 Windows 命令行工具 cmd.exe,你会很高兴地知道,所有你熟悉的命令(例如 cddircls)在 PowerShell 中也可以使用。在后台,这些 DOS “命令”实际上并不是真正的命令,而是命令别名,或者说是伪名,它们将你熟悉的命令转换为 PowerShell 识别的命令。但目前,你不需要了解这些差异——只需将它们视为你熟悉的 DOS 好朋友!

让我们尝试一些命令。如果你在PS>提示符下,并且想查看某个特定目录的内容,首先使用cd命令进入该目录,cd更改目录(change directory)的缩写。这里你将进入Windows目录:

PS> cd .\Windows\
PS C:\Windows>

使用 TAB 完成

请注意,我用点和反斜杠指定了 Windows 目录:.\Windows\。事实上,你无需手动输入这些内容,因为 PowerShell 控制台具有很棒的 Tab 补全功能,允许你重复按 TAB 键来浏览你已经输入的内容所能使用的命令。

例如,如果你输入 GET- 后按 TAB 键,你可以开始滚动所有以 GET- 开头的命令。继续按 TAB 键可以向前浏览命令;按 SHIFT-TAB 键则可以向后浏览。你还可以对参数使用 Tab 补全,我将在“探索 PowerShell 命令”部分中讲解,正如你在 第 6 页 中看到的,输入 Get-Content - 然后按 TAB 键。此次,PowerShell 不再循环浏览命令,而是开始循环浏览 Get-Content 命令的可用参数。当你不确定时,按 TAB 键!

进入 C:\Windows 文件夹后,你可以使用 dir 命令列出当前目录的内容,如 Listing 1-1 中所示。

PS C:\Windows> dir

    Directory: C:\Windows

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        3/18/2019   4:03 PM                addins
d-----         8/9/2019  10:28 AM                ADFS
d-----        7/24/2019   5:39 PM                appcompat
d-----        8/19/2019  12:33 AM                AppPatch
d-----        9/16/2019  10:25 AM                AppReadiness
--snip--

Listing 1-1:使用 dir 命令显示当前目录的内容

输入 cls 将清除屏幕并重新启动一个干净的控制台。如果你熟悉 cmd.exe,可以尝试一些你知道的其他 cmd.exe 命令,看看它们是否有效。请注意,虽然大多数命令有效,但并非所有命令都有效。如果你想知道哪些 cmd.exe 命令在 PowerShell 中存在,你可以在 PowerShell 控制台中输入 Get-Alias,它将返回许多你熟悉的老式 cmd.exe 命令,如下所示:

PS> Get-Alias

这将允许你查看所有内置的别名以及它们映射到的 PowerShell 命令。

探索 PowerShell 命令

像几乎所有语言一样,PowerShell 有 命令,这是指命名的可执行表达式的通用术语。一个命令几乎可以是任何东西——从传统的 ping.exe 工具到我之前提到的 Get-Alias 命令。你甚至可以创建自己的命令。然而,如果你尝试使用一个不存在的命令,你将看到著名的红色错误文本,正如在 Listing 1-2 中所示。

PS> foo
foo : The term 'foo' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a
path was included, verify that the path is correct and try again.
At line:1 char:1
+ foo
+ ~~~
    + CategoryInfo          : ObjectNotFound: (foo:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Listing 1-2:输入未识别命令时显示错误信息。

你可以执行 Get-Command 来查看 PowerShell 默认识别的所有命令列表。你可能会注意到一个常见的模式。大多数命令的名称遵循相同的模式:动词-名词。这是 PowerShell 的独特特点。为了保持语言尽可能直观,微软已经为命令名称设置了规范。虽然遵循这种命名约定是可选的,但强烈推荐在创建自己的命令时使用它。

PowerShell 命令有几种类型:cmdlet、函数、别名,有时还有外部脚本。微软提供的大多数内置命令是 cmdlet,通常是用其他语言(如 C#)编写的命令。通过运行 Get-Command 命令,如 Listing 1-3 中所示,你将看到一个 CommandType 字段。

PS> Get-Command -Name Get-Alias

CommandType     Name                Version    Source
-----------     ----                -------    ------
Cmdlet          Get-Alias           3.1.0.0    Microsoft.PowerShell.Utility

列表 1-3:显示 Get-Alias 命令的类型

函数 是用 PowerShell 编写的命令。你编写函数来完成任务;而 cmdlet 则交给软件开发人员处理。Cmdlet 和函数是你在 PowerShell 中最常用的命令类型。

你将使用 Get-Command 命令来浏览 PowerShell 中可用的众多 cmdlet 和函数。但是正如你刚刚看到的,输入没有参数的 Get-Command 会让你等几秒钟,因为控制台正在滚动显示所有可用的命令。

PowerShell 中有许多命令具有参数,这些参数是你传递给命令的值,用以定制其行为。例如,Get-Command 有多个参数,允许你只返回特定的命令,而不是所有命令。在查看 Get-Command 时,你可能会注意到一些常见的动词,如 GetSetUpdateRemove。如果你猜测所有的 Get 命令都是用来获取信息的,而其他命令则修改信息,你猜对了。在 PowerShell 中,所见即所得。命令的命名直观,并且通常按预期执行。

由于你刚刚开始,你不想改变系统中的任何内容。你确实想从不同来源获取信息。例如,使用 Get-CommandVerb 参数,你可以将庞大的命令列表限制为仅包含使用 Get 动词的命令。要做到这一点,请在提示符下输入以下命令:

PS> Get-Command -Verb Get

你可能会同意,显示的命令太多了,因此你可以通过添加 Noun 参数来进一步限制结果,指定 Content 名词,如 列表 1-4 所示。

PS> Get-Command -Verb Get -Noun Content

CommandType     Name                Version    Source
-----------     ----                -------    ------
Cmdlet          Get-Content         3.1.0.0    Microsoft.PowerShell.Management

列表 1-4:仅显示包含动词 Get 和名词 Content 的命令

如果这些结果对你来说过于狭窄,你也可以使用 Noun 参数而不指定 Verb 参数,如 列表 1-5 所示。

PS> Get-Command -Noun Content

CommandType     Name                Version    Source
-----------     ----                -------    ------
Cmdlet          Add-Content         3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Clear-Content       3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Get-Content         3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Set-Content         3.1.0.0    Microsoft.PowerShell.Management

列表 1-5:仅显示包含名词 Content 的命令

你可以看到 Get-Command 允许你将动词和名词分开。如果你更愿意将整个命令定义为一个单元,你可以改用 Name 参数,指定整个命令名称,如 列表 1-6 所示。

PS> Get-Command -Name Get-Content

CommandType     Name                Version    Source
-----------     ----                -------    ------
Cmdlet          Get-Content         3.1.0.0    Microsoft.PowerShell.Management

列表 1-6:按命令名称查找 Get-Content cmdlet

正如我之前所说,PowerShell 中有许多命令带有可以定制其行为的参数。你可以通过使用强大的 PowerShell 帮助系统来学习命令的参数。

获取帮助

PowerShell 的文档并不独特,但其文档和帮助内容与语言的集成方式堪称艺术。在本节中,你将学习如何在提示窗口中显示命令帮助页面,如何通过 About 主题获取有关语言的更多一般信息,以及如何使用 Update-Help 更新文档。

显示文档

类似于 Linux 中的 man 命令,PowerShell 有 help 命令和 Get-Help cmdlet。如果您有兴趣查看某个 Content cmdlet 的作用,您可以将该命令名称传递给 Get-Help 命令,以检索标准的 SYNOPSISSYNTAXDESCRIPTIONRELATED LINKSREMARKS 帮助部分。这些部分提供了命令的作用、在哪里可以找到该命令的更多信息,甚至一些相关命令。清单 1-7 显示了 Add-Content 命令的文档。

PS> Get-Help Add-Content

NAME
    Add-Content

SYNOPSIS
    Appends content, such as words or data, to a file.

--snip--

清单 1-7:Add-Content 命令的帮助页面

只向 Get-Help 提供命令名称是有用的,但此内容中最有帮助的部分是 Examples 参数。该参数显示了在各种场景下使用该命令的实际示例。尝试在任何命令上运行 Get-Help 命令名称 -Examples,您会注意到几乎所有内置命令都有示例,帮助您理解它们的作用。例如,您可以在 Add-Content cmdlet 上运行该命令,如清单 1-8 所示。

PS> Get-Help Add-Content -Examples

NAME
    Add-Content
SYNOPSIS
    Appends content, such as words or data, to a file.

    -------------------------- EXAMPLE 1 --------------------------

    C:\PS>Add-Content -Path *.txt -Exclude help* -Value "END"

    Description

    -----------

    This command adds "END" to all text files in the current directory,
    except for those with file names that begin with "help."
--snip--

清单 1-8:获取 Add-Content 命令的示例用法

如果您想要更多信息,Get-Help cmdlet 还具有 DetailedFull 参数,它们会提供关于该命令的完整介绍。

了解一般主题

除了单个命令的帮助内容,PowerShell 帮助系统还提供了关于主题,这些是针对更广泛主题和特定命令的帮助片段。例如,在本章中,您将学习 PowerShell 核心命令的一些内容。微软已经创建了一个关于主题,给出了这些命令的总体解释。要查看它,您可以运行 Get-Help about_Core_Commands,如清单 1-9 所示。

PS> Get-Help about_Core_Commands
TOPIC
    about_Core_Commands

SHORT DESCRIPTION
    Lists the cmdlets that are designed for use with Windows PowerShell
    providers.

LONG DESCRIPTION
    Windows PowerShell includes a set of cmdlets that are specifically
    designed to manage the items in the data stores that are exposed by Windows
    PowerShell providers. You can use these cmdlets in the same ways to manage
    all the different types of data that the providers make available to you.
    For more information about providers, type "get-help about_providers".

    For example, you can use the Get-ChildItem cmdlet to list the files in a
    file system directory, the keys under a registry key, or the items that
    are exposed by a provider that you write or download.
 The following is a list of the Windows PowerShell cmdlets that are designed
    for use with providers:

--snip--

清单 1-9:PowerShell 核心命令的相关主题

要获取所有可用的关于主题的完整列表,可以对 Name 参数使用通配符。在 PowerShell 中,通配符字符星号(*)可以作为零个或多个字符的占位符。您可以在 Get-Help 命令的 Name 参数中使用通配符,如清单 1-10 所示。

PS> Get-Help -Name About*

清单 1-10:在 Get-Help 命令的 Name 参数上使用通配符

通过将通配符附加到 About,您要求 PowerShell 搜索所有以 About 开头的主题。如果有多个匹配项,PowerShell 将显示一个列表,并简要介绍每个主题的信息。要获取有关某个匹配项的完整信息,您必须将其直接传递给 Get-Help,如前面在清单 1-9 中所示。

虽然 Get-Help 命令有一个 Name 参数,但你可以直接通过输入 -Name 来传递该参数的值,如 列表 1-10 所示。这被称为使用 位置 参数,它根据参数在命令中的位置(你猜对了)来确定你传递的值。位置参数是许多 PowerShell 命令提供的快捷方式,可以减少输入的按键次数。

更新文档

PowerShell 中的帮助系统是任何想要深入了解语言的人的宝贵财富,但有一个关键特点使这个帮助系统更为出色:它是动态的!文档往往随着时间的推移而变得陈旧。一款产品发布时有文档,之后出现 bug,发布新特性,但系统中的文档却保持不变。PowerShell 通过 可更新帮助 来解决这个问题,它允许内置的 PowerShell cmdlet 以及其他由他人构建的 cmdlet 或函数,通过指向一个互联网 URI 来托管最新的文档。只需输入 Update-Help,PowerShell 就会开始读取系统上的帮助文件并与各种在线位置进行比对。

请注意,虽然所有内置 PowerShell cmdlet 都包含可更新的帮助,但对于任何第三方命令来说,这并不是必需的。另外,文档的更新频率取决于开发者的维护。PowerShell 提供了让开发者编写更好帮助内容的工具,但他们仍然需要保持帮助文件库的最新状态。最后,如果帮助文件存储的位置不再可用,你在运行 Update-Help 时可能会遇到错误。简而言之,不要指望 Update-Help 始终 显示 PowerShell 中每个命令的最新帮助内容。

以管理员身份运行 PowerShell

有时需要以 管理员身份 运行 PowerShell 控制台。这通常发生在你需要修改文件、注册表或任何其他超出你用户个人资料的内容时。例如,前面提到的 Update-Help 命令需要修改系统级别的文件,普通用户无法正常运行该命令。

你可以通过右键点击 Windows PowerShell,然后点击 以管理员身份运行 来以管理员身份运行 PowerShell,如 图 1-2 所示。

Image

图 1-2:以管理员身份运行 PowerShell

总结

在这一章中,你学到了一些有助于入门的命令。开始任何新事物时,你并不知道自己不知道什么。你只需要一些基本的知识,这些知识能够让你自主探索更多内容。通过理解 PowerShell 命令的基础以及如何使用 Get-CommandGet-Help,你现在拥有了开始学习 PowerShell 所需的工具。一个充满激动人心旅程的开始就在前方!

第二章:基本 PowerShell 概念

图片

本章介绍了 PowerShell 中的四个基本概念:变量、数据类型、对象和数据结构。这些概念是几乎所有常见编程语言的基础,但 PowerShell 有一个独特之处:PowerShell 中的每一样东西都是一个对象。

这可能现在对你意义不大,但在你继续本章的学习时,请记住这一点。到本章结束时,你应该能明白这一点有多重要。

变量

变量 是存储 的地方。你可以把变量想象成一个数字盒子。当你想多次使用一个值时,例如,可以把它放在一个盒子里。然后,代替在代码中反复输入相同的数字,你可以将其放入变量中,并在需要该值时调用该变量。但正如你从名称中可能猜到的那样,变量的真正强大之处在于它可以改变:你可以往盒子里加东西,把盒子里的内容换成别的东西,或者拿出里面的东西展示一下,然后再把它放回去。

正如你将在本书后面看到的那样,这种可变性使你能够构建可以处理一般情况的代码,而不是针对某一个特定场景量身定制的代码。本节介绍了使用变量的基本方法。

显示和更改变量

PowerShell 中的所有变量都以美元符号($)开头,这表示你正在调用一个变量,而不是调用 cmdlet、函数、脚本文件或可执行文件。例如,如果你想显示 MaximumHistoryCount 变量的值,你必须在变量名前加上美元符号并调用它,如清单 2-1 所示。

PS> $MaximumHistoryCount
4096

清单 2-1:调用 $MaximumHistoryCount 变量

$MaximumHistoryCount 变量是一个内建变量,用于确定 PowerShell 在命令历史记录中保存的最大命令数;默认值为 4096 条命令。

你可以通过输入变量名(以美元符号开始),然后使用等号(=)和新值来更改变量的值,如清单 2-2 所示。

PS> $MaximumHistoryCount = 200
PS> $MaximumHistoryCount
200

清单 2-2:更改 $MaximumHistoryCount 变量的值

在这里,你将 $MaximumHistoryCount 变量的值更改为 200,这意味着 PowerShell 只会保存最近的 200 条命令。

清单 2-1 和 2-2 使用了已经存在的变量。PowerShell 中的变量大致分为两类:用户定义的变量,由用户创建,以及 自动变量,这些变量在 PowerShell 中已经存在。我们先来看一下用户定义的变量。

用户定义的变量

在使用变量之前,变量需要先存在。尝试在 PowerShell 控制台中输入 $color,如清单 2-3 所示。

PS> $color
The variable '$color' cannot be retrieved because it has not been set.

At line:1 char:1
+ $color
+ ~~~~
 + CategoryInfo          : InvalidOperation: (color:String) [], RuntimeException
    + FullyQualifiedErrorId : VariableIsUndefined

清单 2-3:输入未定义变量会导致错误。

开启严格模式

如果你在列表 2-3 中没有遇到错误,且控制台没有显示任何输出,尝试运行以下命令以开启严格模式:

PS> Set-StrictMode -Version Latest

开启严格模式会告知 PowerShell,在你违反良好编码实践时抛出错误。例如,严格模式会强制 PowerShell 在你引用不存在的对象属性或未定义的变量时返回错误。在编写脚本时,开启此模式被认为是最佳实践,因为它强制你编写更简洁、更可预测的代码。而在 PowerShell 控制台中运行交互式代码时,通常不使用此设置。有关严格模式的更多信息,请运行Get-Help Set-StrictMode -Examples

在列表 2-3 中,你尝试在$color变量尚不存在之前引用它,结果导致了一个错误。要创建一个变量,你需要声明它——即声明它存在——然后赋值给它(或初始化它)。你可以同时进行这两个操作,像列表 2-4 所示,创建一个值为blue的变量$color。你可以使用与改变$MaximumHistoryCount值相同的技巧来为变量赋值——输入变量名,后跟等号,再加上值。

PS> $color = 'blue'

列表 2-4:创建一个值为bluecolor变量

一旦你创建了变量并为其赋值,你可以通过在控制台中键入变量名来引用它(如列表 2-5 所示)。

PS> $color
blue

列表 2-5:检查变量的值

变量的值不会改变,除非有某个操作或某人明确地改变它。你可以多次调用$color变量,它每次都会返回值blue,直到该变量被重新定义。

当你使用等号定义变量时(如列表 2-4 所示),你正在做的事情与使用Set-Variable命令是一样的。同样,当你在控制台中键入一个变量,它输出该值时,像列表 2-5 所示,你也在做的事情与使用Get-Variable命令是一样的。列表 2-6 通过这些命令重新创建了列表 2-4 和 2-5。

PS> Set-Variable -Name color -Value blue

PS> Get-Variable -Name color

Name                           Value
----                           -----
color                          blue

列表 2-6:使用Set-VariableGet-Variable命令创建变量并显示其值

你也可以使用Get-Variable返回所有可用的变量(如列表 2-7 所示)。

PS> Get-Variable 

Name                           Value
----                           -----
$                              Get-PSDrive
?                              True
^                              Get-PSDrive
args                           {}
color                          blue
--snip--

列表 2-7:使用Get-Variable返回所有变量。

这个命令将列出当前内存中的所有变量,但请注意,有些变量是你尚未定义的。你将在下一节中查看这种类型的变量。

自动变量

之前我介绍了自动变量,这些是 PowerShell 自己使用的预定义变量。虽然 PowerShell 允许你更改其中一些变量,就像你在示例 2-2 中所做的那样,但我通常不建议这样做,因为可能会引发意外后果。通常情况下,你应该将自动变量视为只读。 (现在可能是时候将$MaximumHistoryCount恢复为 4096 了!)

本节涵盖了一些你可能会使用的自动变量:$null变量、$LASTEXITCODE和偏好设置变量。

$null变量

$null变量是一个奇怪的变量:它代表“无”。将$null赋值给一个变量,可以让你创建该变量,但不为其分配实际的值,就像在示例 2-8 中所示。

PS> $foo = $null
PS> $foo
PS> $bar
The variable '$bar' cannot be retrieved because it has not been set.
At line:1 char:1
+ $bar
+ ~~~~
    + CategoryInfo          : InvalidOperation: (bar:String) [], RuntimeException
    + FullyQualifiedErrorId : VariableIsUndefined

示例 2-8:将变量赋值为$null

在这里,你将$null赋值给$foo变量。然后,当你调用$foo时,不会显示任何内容,但不会出现错误,因为 PowerShell 能够识别该变量。

你可以通过向Get-Variable命令传递参数来查看 PowerShell 识别的变量。在示例 2-9 中,你可以看到 PowerShell 知道$foo变量存在,但没有识别出$bar变量。

PS> Get-Variable -Name foo

Name                           Value
----                           -----
foo

PS> Get-Variable -Name bar
Get-Variable : Cannot find a variable with the name 'bar'.
At line:1 char:1
+ Get-Variable -Name bar
+ ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (bar:String) [Get-Variable], ItemNotFoundException
    + FullyQualifiedErrorId : VariableNotFound,Microsoft.PowerShell.Commands.GetVariableCommand

示例 2-9:使用Get-Variable查找变量

你可能会好奇为什么我们需要将某些东西定义为$null。其实,$null是非常有用的。例如,正如你将在本章后面看到的,通常你会根据其他事情的结果为一个变量赋值,比如某个函数的输出。如果你检查该变量,发现其值仍然是$null,那就意味着函数出了问题,你可以据此采取相应的措施。

LASTEXITCODE 变量

另一个常用的自动变量是$LASTEXITCODE。PowerShell 允许你调用外部可执行应用程序,比如老式的ping.exe,它 ping 一个网站并获取响应。当外部应用程序运行结束时,它会以一个退出代码返回代码结束,该代码表示一个消息。通常,0 表示成功,其他任何值都表示失败或其他异常。对于ping.exe,0 表示能够成功 ping 到一个节点,1 表示无法 ping 通。

*ping.exe*运行时,正如在示例 2-10 中所示,你会看到预期的输出,但不会看到退出代码。这是因为退出代码被隐藏在$LASTEXITCODE中。$LASTEXITCODE的值始终是最后执行的应用程序的退出代码。示例 2-10 会 ping google.com,返回其退出代码,然后 ping 一个不存在的域名并返回其退出代码。

PS> ping.exe -n 1 dfdfdfdfd.com

Pinging dfdfdfdfd.com [14.63.216.242] with 32 bytes of data:
Request timed out.

Ping statistics for 14.63.216.242:
    Packets: Sent = 1, Received = 0, Lost = 1 (100% loss),
PS> $LASTEXITCODE
1
PS> ping.exe -n 1 google.com

Pinging google.com [2607:f8b0:4004:80c::200e] with 32 bytes of data:
Reply from 2607:f8b0:4004:80c::200e: time=47ms

Ping statistics for 2607:f8b0:4004:80c::200e:
    Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 47ms, Maximum = 47ms, Average = 47ms
PS> $LASTEXITCODE
0

示例 2-10:使用ping.exe演示$LASTEXITCODE变量

当你 ping google.com 时,$LASTEXITCODE为 0,而当你 ping 虚假的域名dfdfdfdfd.com时,$LASTEXITCODE的值为 1。

偏好设置变量

PowerShell 有一种称为首选项变量的自动变量类型。这些变量控制着各种输出流的默认行为:ErrorWarningVerboseDebugInformation

你可以通过运行Get-Variable并过滤出所有以Preference结尾的变量来查找所有首选项变量,示例如下:

PS> Get-Variable -Name *Preference

Name                           Value
----                           -----
ConfirmPreference              High
DebugPreference                SilentlyContinue
ErrorActionPreference          Continue
InformationPreference          SilentlyContinue
ProgressPreference             Continue
VerbosePreference              SilentlyContinue
WarningPreference              Continue
WhatIfPreference               False

这些变量可用于配置 PowerShell 可以返回的各种类型的输出。例如,如果你曾经犯过错误并看到过那些难看的红色文本,那么你就见识过Error输出流。运行以下命令以生成错误信息:

PS> Get-Variable -Name 'doesnotexist'
Get-Variable : Cannot find a variable with the name 'doesnotexist'.
At line:1 char:1
+ Get-Variable -Name 'doesnotexist'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (doesnotexist:String) [Get-Variable],
                              ItemNotFoundException
    + FullyQualifiedErrorId : VariableNotFound,Microsoft.PowerShell.Commands.GetVariableCommand

你应该看到类似的错误信息,因为这是Error流的默认行为。如果出于某种原因你不想被这些错误文本打扰,并希望什么都不发生,你可以将$ErrorActionPreference变量重新定义为SilentlyContinueIgnore,这两种方式都会告诉 PowerShell 不输出任何错误文本:

PS> $ErrorActionPreference = 'SilentlyContinue'
PS> Get-Variable -Name 'doesnotexist'
PS>

如你所见,没有输出错误文本。忽略错误输出通常被认为是不好的做法,因此请在继续之前将$ErrorActionPreference的值改回Continue。有关首选项变量的更多信息,请通过运行 Get-Help about_Preference_Variables 查看 about_help 内容。

数据类型

PowerShell 变量有多种形式或类型。PowerShell 的数据类型的所有细节超出了本章的讨论范围。你需要知道的是,PowerShell 有几种数据类型——包括布尔值、字符串和整数——而且你可以在不产生错误的情况下更改变量的数据类型。以下代码应该能够顺利运行:

PS> $foo = 1
PS> $foo = 'one'
PS> $foo = $true

这是因为 PowerShell 可以根据你提供的值来推断数据类型。背后发生的事情有点复杂,不适合本书的讨论,但理解基本类型以及它们如何交互是很重要的。

布尔值

几乎所有编程语言都使用布尔值,它们有真或假的值(1 或 0)。布尔值用于表示二元条件,比如开关的开或关。在 PowerShell 中,布尔值被称为bools,这两个布尔值分别由自动变量$true$false表示。这些自动变量是硬编码到 PowerShell 中的,无法更改。列表 2-11 展示了如何将变量设置为$true$false

PS> $isOn = $true
PS> $isOn 
True

列表 2-11:创建一个布尔变量

你将在第四章中看到更多的布尔值。

整数与浮点数

你可以通过整数或浮点数据类型在 PowerShell 中表示数字。

整数类型

整数数据类型仅保存整数,并会将任何小数输入四舍五入为最接近的整数。整数数据类型分为有符号无符号类型。有符号数据类型可以存储正数和负数;无符号数据类型只存储没有符号的数值。

默认情况下,PowerShell 使用 32 位有符号 Int32 类型来存储整数。位数决定了变量能够存储的数字的大小(或小),在这种情况下,范围是从 -2,147,483,648 到 2,147,483,647。对于超出此范围的数字,你可以使用 64 位有符号 Int64 类型,范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。

列表 2-12 展示了 PowerShell 如何处理 Int32 类型的示例。

❶ PS> $num = 1
   PS> $num
   1
❷ PS> $num.GetType().name
   Int32
❸ PS> $num = 1.5
   PS> $num.GetType().name
   Double
❹ PS> [Int32]$num
   2

列表 2-12:使用 Int 类型存储不同的值

让我们逐步了解每一个步骤。别担心所有的语法,现在关注输出。首先,你创建一个变量 $num 并赋值为 1 ❶。接下来,你检查 $num 的类型 ❷,发现 PowerShell 将 1 解释为 Int32。然后,你将 $num 改为持有一个小数值 ❸,再次检查类型,发现 PowerShell 已经将类型改为 Double。这是因为 PowerShell 会根据变量的值来更改其类型。但你可以通过强制转换该变量来让 PowerShell 将其视为某种特定类型,如你在最后通过在 $num 前加上 [Int32] 语法所做的那样 ❹。如你所见,当强制将 1.5 视为整数时,PowerShell 会将其四舍五入为 2。

现在让我们来看一下 Double 类型。

浮动点类型

Double 类型属于称为浮动点变量的更广泛类别。尽管它们可以用于表示整数,但浮动点变量最常用于表示小数。浮动点变量的另一个主要类型是 Float。我不会深入讨论 FloatDouble 类型的内部表示。你需要知道的是,虽然 FloatDouble 能够表示小数,但这些类型可能会不精确,如 列表 2-13 所示。

PS> $num = 0.1234567910
PS> $num.GetType().name
Double
PS> $num + $num
0.2469135782 
PS> [Float]$num + [Float]$num
0.246913582086563

列表 2-13:浮动点类型的精度错误

如你所见,PowerShell 默认使用 Double 类型。但请注意,当你将 $num 与自身相加并强制将两者转换为 Float 类型时,结果会非常奇怪。原因超出了本书的范围,但要注意,使用 FloatDouble 时可能会发生类似的错误。

字符串

你已经见过这种类型的变量。当你在 列表 2-4 中定义 $color 变量时,你并没有直接输入 $color = blue。相反,你将值括在单引号中,这表示 PowerShell 该值是一串字母,或称为字符串。如果你尝试在没有引号的情况下将 blue 值赋给 $color,PowerShell 将返回一个错误:

PS> $color = blue
blue : The term 'blue' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the
path is correct and try again.
At line:1 char:10
+ $color = blue
+          ~~~~
    + CategoryInfo          : ObjectNotFound: (blue:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

如果没有引号,PowerShell 会将 blue 解释为一个命令并尝试执行它。因为命令 blue 不存在,所以 PowerShell 会返回一个错误信息。要正确定义字符串,你需要在值周围使用引号。

组合字符串和变量

字符串不限于单词,它们也可以是短语和句子。例如,你可以给$sentence赋值如下字符串:

PS> $sentence = "Today, you learned that PowerShell loves the color blue"
PS> $sentence
Today, you learned that PowerShell loves the color blue

但也许你想使用相同的句子,不过将PowerShellblue作为变量的值。例如,假设你有一个叫做$name的变量,一个叫做$language的变量,和一个叫做$color的变量?列表 2-14 通过使用其他变量来定义这些变量。

PS> $language = 'PowerShell'
PS> $color = 'blue'

PS> $sentence = "Today, you learned that $language loves the color $color"
PS> $sentence
Today, you learned that PowerShell loves the color blue

列表 2-14:在字符串中插入变量

注意双引号的使用。将句子用单引号括起来并不能达到预期的效果:

PS> 'Today, $name learned that $language loves the color $color'
Today, $name learned that $language loves the color $color

这不仅仅是一个奇怪的错误。在 PowerShell 中,单引号和双引号之间有一个重要的区别。

使用双引号与单引号

当你给一个变量赋值一个简单的字符串时,可以使用单引号或双引号,如列表 2-15 所示。

PS> $color = "yellow"
PS> $color
yellow
PS> $color = 'red'
PS> $color
red
PS> $color = ''
PS> $color
PS> $color = "blue"
PS> $color
blue

列表 2-15:通过使用单引号和双引号来改变变量值

如你所见,定义一个简单字符串时使用单引号或双引号都没有关系。那么,为什么当字符串中有变量时就变得重要了呢?答案与变量插值,或变量扩展有关。通常,当你单独输入$color并按下 ENTER 键时,PowerShell 会插值,或扩展那个变量。这些都是 fancy 的术语,意味着 PowerShell 正在读取变量中的值,或者说是打开箱子让你看里面的内容。当你使用双引号调用变量时,同样的事情会发生:变量会被扩展,正如你在列表 2-16 中看到的那样。

PS> "$color"
blue
PS> '$color'
$color

列表 2-16:字符串中的变量行为

但是注意当你使用单引号时会发生什么:控制台输出的是变量本身,而不是它的值。单引号告诉 PowerShell 你输入的就是确切的内容,无论是像blue这样的单词,还是看起来像一个叫做$color的变量。对 PowerShell 来说,这并不重要。它不会超越单引号中的值。所以,当你在单引号中使用变量时,PowerShell 不知道要扩展该变量的值。这就是为什么在将变量插入字符串时需要使用双引号的原因。

关于布尔值、整数和字符串还有很多内容要讲。但现在,让我们退后一步,看看一些更一般的内容:对象。

对象

在 PowerShell 中,一切都是对象。从技术上讲,对象是特定模板(称为类)的单个实例。指定了一个对象将包含的内容。对象的类决定了它的方法,即可以对该对象执行的操作。换句话说,方法就是对象能做的所有事情。例如,一个列表对象可能有一个sort()方法,当调用时,它将排序该列表。同样,对象的类决定了它的属性,即对象的变量。你可以把属性看作是关于对象的所有数据。以列表对象为例,你可能会有一个length属性,它存储列表中元素的数量。有时,一个类会为对象的属性提供默认值,但更多情况下,这些值是你在工作中为对象提供的。

但这一切都是非常抽象的。让我们考虑一个例子:一辆车。车从设计阶段的一个计划开始。这个计划或模板定义了车应该是什么样子,应该有什么样的引擎,应该有什么样的底盘,等等。计划还规定了车完成后能够做什么——前进、倒退、开关天窗。你可以把这个计划看作是车的类。

每辆车都是从这个类构建的,并且该车所有特定的属性和方法都被添加到它身上。一辆车可能是蓝色的,而同一型号的车可能是红色的,另外一辆车可能有不同的变速箱。这些属性是特定车对象的属性。同样,每辆车都会前进、倒退,并且有相同的方法来开关天窗。这些操作就是车的方式。

现在,通过对对象如何工作的总体了解,让我们动手实践,使用 PowerShell。

检查属性

首先,让我们创建一个简单的对象,这样你可以拆解它,揭示 PowerShell 对象的各个方面。列表 2-17 创建了一个名为$color的简单字符串对象。

PS> $color = 'red'
PS> $color
red

列表 2-17:创建字符串对象

请注意,当你调用$color时,你只会得到变量的值。但通常情况下,因为它们是对象,变量包含的信息不仅仅是它们的值。它们还有属性。

要查看对象的属性,你将使用Select-Object命令和Property参数。你将向Property传递一个星号参数,如列表 2-18 所示,以告诉 PowerShell 返回它找到的所有内容。

PS>  Select-Object -InputObject $color -Property *

Length
------
     3

列表 2-18:调查对象属性

如你所见,$color字符串只有一个属性,叫做Length

你可以通过使用点表示法直接引用Length属性:你使用对象的名称,后跟一个点,再加上你想要访问的属性名称(见列表 2-19)。

PS> $color.Length
3

列表 2-19:使用点表示法检查对象的属性

像这样引用对象将随着时间的推移变得得心应手。

使用 Get-Member cmdlet

使用 Select-Object,你发现 $color 字符串只有一个属性。但请记住,对象有时也包含方法。要查看此字符串对象上所有的 方法属性,你可以使用 Get-Member cmdlet(清单 2-20);这个 cmdlet 会是你很长一段时间里的好帮手。它是一个快速列出特定对象所有属性和方法的简便方法,统称为对象的 成员

PS> Get-Member -InputObject $color

   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone(), System.Object ICloneable.Clone()
CompareTo        Method                int CompareTo(System.Object value),
                                       int CompareTo(string strB), int IComparab...
Contains         Method                bool Contains(string value)
CopyTo           Method                void CopyTo(int sourceIndex, char[] destination,
                                       int destinationIndex, int co...
EndsWith         Method                bool EndsWith(string value),
                                       bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj),
                                       bool Equals(string value), bool Equals(string...
--snip--
Length           Property              int Length {get;}

清单 2-20:使用 Get-Member 来调查对象的属性和方法

现在我们开始深入探讨!事实证明,你的简单字符串对象有很多相关的方法。还有许多其他方法值得探索,但并不是所有都在这里展示。一个对象的方法和属性数量取决于它的父类。

调用方法

你可以使用点符号来引用方法。然而,与属性不同,方法总是以一对开闭圆括号结尾,并且可以接受一个或多个参数。

例如,假设你想删除 $color 变量中的一个字符。你可以使用 Remove() 方法从字符串中删除字符。让我们通过 清单 2-21 中的代码,来聚焦 $colorRemove() 方法。

PS> Get-Member -InputObject $color –Name Remove
Name   MemberType Definition
----   ---------- ----------
Remove Method     string Remove(int startIndex, int count), string Remove(int startIndex)

清单 2-21:查看字符串的 Remove() 方法

如你所见,有两个定义。这意味着你可以通过两种方式使用该方法:一种是带有 startIndexcount 参数,另一种是仅使用 startIndex

所以,要删除 $color 中的第二个字符,你需要指定你希望开始删除字符的位置,这个位置我们称之为 索引。索引从 0 开始,所以第一个字母的起始位置是 0,第二个字母的索引是 1,依此类推。通过索引,你还可以指定希望删除的字符数,方法是使用逗号分隔参数,如 清单 2-22 所示。

PS> $color.Remove(1,1)
Rd
PS> $color
red

清单 2-22:调用方法

使用索引 1,你告诉 PowerShell 从字符串的第二个字符开始删除;第二个参数告诉 PowerShell 只删除一个字符。所以你得到 Rd。但是请注意,Remove() 方法并不会永久改变字符串变量的值。如果你想保留这个更改,需要将 Remove() 方法的输出赋值给一个变量,如 清单 2-23 所示。

PS> $newColor = $color.Remove(1,1)
PS> $newColor
Rd

清单 2-23:捕获字符串上 Remove() 方法的输出

注意

如果你需要知道某个方法是返回一个对象(如 Remove() 所做的)还是修改一个现有对象,你可以查看它的描述。如你所见,在示例 2-21 中,Remove() 的定义前面有一个字符串;这意味着该函数返回一个新的字符串。前面有 void 的函数通常会修改现有对象。第六章将更深入地讨论这个话题。

在这些示例中,你使用了最简单的对象类型之一——字符串。在下一节中,你将了解一些更复杂的对象。

数据结构

数据结构是组织多个数据项的一种方式。与它们所组织的数据类似,PowerShell 中的数据结构由存储在变量中的对象表示。它们主要有三种类型:数组、ArrayList 和哈希表。

数组

到目前为止,我把变量描述为一个盒子。但如果一个简单的变量(例如Float类型)是一个单独的盒子,那么数组就是一大堆用胶带粘在一起的盒子——由单个变量表示的一系列项目。

通常你需要几个相关的变量——比如一组标准颜色。与其将每个颜色存储为单独的字符串,然后引用这些单独的变量,不如将所有这些颜色存储在一个单一的数据结构中,这样效率更高。本节将向你展示如何创建、访问、修改和向数组中添加元素。

定义数组

首先,让我们定义一个名为 $colorPicker 的变量,并将其赋值为一个包含四种颜色的字符串数组。为此,你使用 @ 符号,后跟四个字符串(用逗号分隔)放在括号内,如示例 2-24 所示。

PS> $colorPicker = @('blue','white','yellow','black')
PS> $colorPicker
blue
white
yellow
black

示例 2-24:创建数组

@符号后面跟着一个左括号和零个或多个由逗号分隔的元素,表示你想要在 PowerShell 中创建一个数组。

注意,在调用 $colorPicker 后,PowerShell 会将数组的每个元素显示在新的一行中。在下一节中,你将学习如何单独访问每个元素。

读取数组元素

要访问数组中的元素,你需要使用数组的名称,后跟一对方括号([]),括号内包含你想访问的元素的索引。与字符串字符一样,数组的编号从 0 开始,所以第一个元素的索引是 0,第二个是 1,以此类推。在 PowerShell 中,使用 -1 作为索引将返回最后一个元素。

示例 2-25 访问我们 $colorPicker 数组中的多个元素。

PS> $colorPicker[0]
blue
PS> $colorPicker[2]
yellow
PS> $colorPicker[3]
black
PS> $colorPicker[4]
Index was outside the bounds of the array.
At line:1 char:1
+ $colorPicker[4]
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], IndexOutOfRangeException
    + FullyQualifiedErrorId : System.IndexOutOfRangeException

示例 2-25:读取数组元素

如你所见,如果你试图指定一个在数组中不存在的索引号,PowerShell 会返回一个错误信息。

要同时访问数组中的多个元素,你可以在两个数字之间使用范围操作符..)。范围操作符会使 PowerShell 返回这两个数字以及它们之间的每一个数字,像这样:

PS> 1..3
1
2
3

要使用范围操作符访问数组中的多个项目,你需要使用一个索引范围,如下所示:

PS> $colorPicker[1..3]
white
yellow
black

现在你已经了解了如何访问数组中的元素,让我们来看看如何更改它们。

修改数组中的元素

如果你想更改数组中的元素,你不需要重新定义整个数组。你可以通过其索引引用某个元素,并使用等号为其赋予一个新值,如 清单 2-26 所示。

PS> $colorPicker[3]
black
PS> $colorPicker[3] = 'white'
PS> $colorPicker[3]
white

清单 2-26:修改数组中的元素

在修改元素之前,确保通过将元素显示到控制台来仔细检查索引号是否正确。

向数组添加元素

你可以使用加法操作符 (+) 向数组添加元素,如 清单 2-27 所示。

PS> $colorPicker = $colorPicker + 'orange'
PS> $colorPicker
blue
white
yellow
white
orange

清单 2-27:向数组添加单个元素

请注意,在等号两边你都输入了 $colorPicker。这是因为你要求 PowerShell 插值 $colorPicker 变量,然后添加一个新元素。

+ 方法有效,但有一种更快捷、更易读的方法。你可以使用加号和等号一起形成 +=(见 清单 2-28)。

PS> $colorPicker += 'brown'
PS> $colorPicker
blue
white
yellow
white
orange
brown

清单 2-28:使用 += 快捷方式向数组添加元素

+= 操作符告诉 PowerShell 将该项添加到现有数组中。这个快捷方式可以避免你两次输入数组名,使用它比输入完整的语法要常见得多。

你还可以将数组添加到其他数组中。假设你想把粉色和青色添加到你的 $colorPicker 示例中。清单 2-29 定义了另一个仅包含这两种颜色的数组,并像在 清单 2-28 中一样将它们添加进来。

PS> $colorPicker += @('pink','cyan')
PS> $colorPicker
blue
white
yellow
white
orange
brown
pink
cyan

清单 2-29:一次向数组添加多个元素

一次添加多个元素可以节省你很多时间,尤其是在你创建一个包含大量元素的数组时。请注意,PowerShell 会将任何以逗号分隔的值集视为数组,你无需显式使用 @ 或括号。

不幸的是,没有类似于 += 的操作符来从数组中删除元素。从数组中删除元素比你想象的更复杂,我们在这里不会详细介绍。要了解原因,请继续阅读!

数组列表(ArrayLists)

当你向数组添加元素时,会发生一些奇怪的事情。每次你向数组添加元素时,实际上是在通过旧的(插值过的)数组和新元素创建一个新的数组。当你从数组中删除元素时也会发生同样的事情:PowerShell 会销毁旧数组并创建一个新数组。这是因为 PowerShell 中的数组大小是固定的。当你改变它们时,无法修改大小,因此必须创建一个新数组。对于像我们之前使用的小数组,你可能不会注意到这种情况。但当你开始处理庞大的数组,包含数万或数十万个元素时,你会看到明显的性能下降。

如果你知道你必须从数组中添加或移除许多元素,我建议你使用另一种数据结构,叫做ArrayList。ArrayList 的行为几乎与典型的 PowerShell 数组相同,但有一个关键的区别:它们没有固定大小。它们可以动态调整以适应添加或移除的元素,在处理大量数据时提供更高的性能。

定义 ArrayList 与定义数组完全相同,只不过你需要将其转换为 ArrayList。示例 2-30 重新创建了颜色选择器数组,但将其转换为System.Collections.ArrayList类型。

PS> $colorPicker = [System.Collections.ArrayList]@('blue','white','yellow','black')
PS> $colorPicker
blue
white
yellow
black

示例 2-30:创建 ArrayList

与数组一样,当你调用一个 ArrayList 时,每个项都会显示在单独的一行上。

向 ArrayList 添加元素

要向 ArrayList 添加或移除元素而不销毁它,你可以使用其方法。你可以使用Add()Remove()方法来向 ArrayList 添加或移除项。示例 2-31 使用了Add()方法并将新元素放在方法的括号内。

PS> $colorPicker.Add('gray')
4

示例 2-31:向 ArrayList 添加单个项

注意输出:数字 4 是你添加的新元素的索引。通常,你不会使用这个数字,所以可以将Add()方法的输出发送到$null变量,避免其输出任何内容,如示例 2-32 所示。

PS> $null = $colorPicker.Add('gray')

示例 2-32:将输出发送到$null

有几种方法可以取消 PowerShell 命令的输出,但将输出分配给$null能提供最佳性能,因为$null变量无法重新赋值。

从 ArrayList 中移除元素

你也可以以类似的方式移除元素,使用Remove()方法。例如,如果你想从 ArrayList 中移除值gray,可以将值放在方法的括号内,如示例 2-33 所示。

PS> $colorPicker.Remove('gray')

示例 2-33:从 ArrayList 中移除项

请注意,要移除一个项目,你不必知道索引号。你可以通过实际值引用元素——在这种情况下是gray。如果数组中有多个相同值的元素,PowerShell 将移除离 ArrayList 开头最近的元素。

在像这样的简单示例中很难看到性能差异。但是,ArrayList 在处理大数据集时比数组表现得更好。与大多数编程选择一样,你需要分析你的具体情况,来确定是使用数组还是 ArrayList 更合适。通常来说,你处理的项目集合越大,使用 ArrayList 越好。如果你处理的是少于 100 个元素的小数组,你几乎不会发现数组和 ArrayList 之间有什么区别。

哈希表

当你只需要根据列表中的位置关联数据时,数组和 ArrayList 很有用。但有时你需要更加直接的方式:一种关联两段数据的方式。例如,你可能有一个用户名列表,想要将其匹配到真实姓名。在这种情况下,你可以使用 哈希表(或 字典),这是 PowerShell 中包含 键值对 列表的数据结构。你不使用数字索引,而是给 PowerShell 提供一个输入,称为 ,它返回与该键关联的 。因此,在我们的例子中,你可以使用用户名作为哈希表的索引,它将返回该用户的真实姓名。

示例 2-34 定义了一个名为 $users 的哈希表,其中包含三名用户的信息。

PS> $users = @{
 abertram = 'Adam Bertram'
 raquelcer = 'Raquel Cerillo'
 zheng21 = 'Justin Zheng'
}
PS> $users
Name                           Value
----                           -----
abertram                       Adam Bertram
raquelcer                      Raquel Cerillo
zheng21                        Justin Zheng

示例 2-34:创建哈希表

PowerShell 不允许你定义具有重复键的哈希表。每个键必须唯一地指向一个值,而这个值可以是一个数组,甚至是另一个哈希表!

从哈希表中读取元素

要访问哈希表中的特定值,你可以使用它的键。你可以通过两种方式来实现这一点。假设你想查找用户 abertram 的真实姓名,你可以使用 示例 2-35 中展示的任意两种方法。

PS> $users['abertram']
Adam Bertram
PS> $users.abertram
Adam Bertram

示例 2-35:访问哈希表的值

这两种选项有细微的差别,但现在你可以选择任何你偏好的方法。

示例 2-35 中的第二条命令使用了一个属性:$users.abertram。PowerShell 会将每个键添加到对象的属性中。如果你想查看哈希表中的所有键和值,可以访问 KeysValues 属性,如 示例 2-36 所示。

PS> $users.Keys
abertram                       
raquelcer                      
zheng21                        
PS> $users.Values
Adam Bertram
Raquel Cerillo
Justin Zheng

示例 2-36:读取哈希表的键和值

如果你想查看哈希表(或任何对象)的 所有 属性,你可以运行以下命令:

PS> Select-Object -InputObject $yourobject -Property *
添加和修改哈希表项

要向哈希表中添加元素,你可以使用 Add() 方法,或者通过使用方括号和等号创建一个新索引。两种方法都在 示例 2-37 中展示。

PS> $users.Add('natice', 'Natalie Ice')
PS> $users['phrigo'] = 'Phil Rigo'

示例 2-37:向哈希表添加项

现在你的哈希表存储了五个用户。如果你需要修改哈希表中的某个值,应该怎么办?

当你修改哈希表时,最好检查你想要修改的键值对是否存在。要检查一个键是否已经存在于哈希表中,你可以使用 ContainsKey() 方法,这是 PowerShell 中每个哈希表的组成部分。当哈希表中包含该键时,它将返回 True;否则,返回 False,如 示例 2-38 所示。

PS> $users.ContainsKey('johnnyq')
False

示例 2-38:检查哈希表中的项

一旦你确认键存在于哈希表中,你可以通过使用一个简单的等号来修改其值,如 示例 2-39 所示。

PS> $users['phrigo'] = 'Phoebe Rigo'
PS> $users['phrigo']
Phoebe Rigo

示例 2-39:修改哈希表值

如你所见,你可以通过几种方式向哈希表中添加项目。正如你将在下一部分看到的,删除哈希表中的项只有一种方法。

从哈希表中删除项

与 ArrayList 一样,哈希表有一个 Remove() 方法。只需调用它并传入你想删除项的键值,如清单 2-40 所示。

PS> $users.Remove('natice')

清单 2-40:从哈希表中删除一项

你的一个用户应该已经不在了,但你可以调用哈希表来进行双重检查。记住,你可以使用 Keys 属性来提醒自己任何键的名称。

创建自定义对象

到目前为止,在本章中,你一直在创建和使用 PowerShell 内置的对象类型。大多数时候,你可以使用这些类型,避免自己创建对象。但有时候,你需要创建一个自定义对象,定义你自己的属性和方法。

清单 2-41 使用 New-Object cmdlet 定义了一个 PSCustomObject 类型的新对象。

PS> $myFirstCustomObject = New-Object -TypeName PSCustomObject

清单 2-41:使用 New-Object 创建自定义对象

这个例子使用了 New-Object 命令,但你也可以通过使用等号和强制转换来做同样的事情,如清单 2-42 所示。你定义一个哈希表,其中键是属性名称,值是属性值,然后将其强制转换为 PSCustomObject

PS> $myFirstCustomObject = [PSCustomObject]@{OSBuild = 'x'; OSVersion = 'y'}

清单 2-42:使用 PSCustomObject 类型加速器创建自定义对象

请注意,清单 2-42 使用分号()来分隔键和值的定义。

一旦你有了自定义对象,你就可以像使用任何其他对象一样使用它。清单 2-43 将我们的自定义对象传递给 Get_Member cmdlet 来检查它是否为 PSCustomObject 类型。

PS> Get-Member  -InputObject $myFirstCustomObject

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
OSBuild     NoteProperty string OSBuild=OSBuild
OSVersion   NoteProperty string OSVersion=Version

清单 2-43:调查自定义对象的属性和方法

如你所见,你的对象已经有了一些预先存在的方法(例如,其中一个返回对象的类型!),以及你在清单 2-42 中创建对象时定义的属性。

让我们通过使用点表示法来访问这些属性:

PS> $myFirstCustomObject.OSBuild
x
PS> $myFirstCustomObject.OSVersion
y

看起来不错!在本书的其余部分,你会经常使用 PSCustomObject 对象。它们是强大的工具,让你能够创建更加灵活的代码。

总结

到目前为止,你应该对对象、变量和数据类型有一个大致的了解。如果你仍然不理解这些概念,请重新阅读这一章。这是我们将要讨论的最基础的内容之一。对这些概念有一个高层次的理解将使得本书的其他部分更容易理解。

下一章将介绍两种在 PowerShell 中组合命令的方法:管道和脚本。

第三章:合并命令

Images

到目前为止,你一直使用 PowerShell 控制台一次执行一个命令。对于简单的代码来说,这没什么问题:你运行需要的命令,如果需要再运行一个命令,也可以这样做。但对于较大的项目来说,单独调用每个命令太耗时了。幸运的是,你可以合并命令,使它们作为一个整体进行调用。在本章中,你将学习两种合并命令的方法:通过使用 PowerShell 管道以及将代码保存在外部脚本中。

启动 Windows 服务

为了说明为什么需要合并命令,首先让我们用传统的方法做一个简单的示例。你将使用两个命令:Get-Service,该命令查询 Windows 服务并返回相关信息;以及 Start-Service,该命令启动 Windows 服务。如示例 3-1 所示,使用 Get-Service 确保服务存在,然后使用 Start-Service 启动它。

PS> $serviceName = 'wuauserv'
PS> Get-Service -Name $serviceName
Status   Name               DisplayName
------   ----               -----------
Running  wuauserv           Windows Update
PS> Start-Service -Name $serviceName

示例 3-1:使用 Name 参数查找服务并启动它

你运行 Get-Service 只是为了确保 PowerShell 不会抛出错误。很可能该服务已经在运行。如果是这样,Start-Service 将只会将控制权返回给控制台。

当你只启动一个服务时,像这样运行命令并不会特别费劲。但如果你需要处理数百个服务,你可以想象它会变得多么单调。让我们来看看如何简化这个问题。

使用管道

简化代码的第一种方法是通过 PowerShell 管道 将命令链接在一起,管道是一种工具,可以将一个命令的输出直接作为另一个命令的输入。要使用管道,在两个命令之间使用 管道操作符 (|),像这样:

PS> command1 | command2

在这里,命令 1 的输出被管道传递到命令 2,成为命令 2 的输入。管道中的最后一个命令将输出到控制台。

许多 shell 脚本语言,包括 cmd.exe 和 bash,都使用管道。但 PowerShell 中管道的独特之处在于它传递的是对象,而不是简单的字符串。本章稍后会讲解这一过程,但现在,让我们使用管道重新编写示例 3-1 中的代码。

在命令之间传递对象

要将 Get-Service 的输出传递给 Start-Service,请使用示例 3-2 中的代码。

PS> Get-Service -Name 'wuauserv' | Start-Service

示例 3-2:将现有服务通过管道传递给 Start-Service 命令

在示例 3-1 中,你使用了 Name 参数来告诉 Start-Service 命令要启动哪个服务。但在这个示例中,你不需要指定任何参数,因为 PowerShell 会为你处理这一切。它会查看 Get-Service 的输出,决定应该将哪些值传递给 Start-Service,并将这些值与 Start-Service 需要的参数进行匹配。

如果你愿意,你可以将示例 3-2 中的代码重写为完全不使用参数:

PS> 'wuauserv' | Get-Service | Start-Service

PowerShell 将字符串 wuauserv 发送到 Get-Service,然后将 Get-Service 的输出传递到 Start-Service ——这一切都不需要你做任何指定!你已经将三个独立的命令合并为一行,但每次你想启动服务时,仍然需要重新输入这一行。在下一节中,你将看到如何使用一行命令启动你需要的任意多个服务。

在命令之间传递数组

在文本编辑器中,如记事本,创建一个名为 Services.txt 的文本文件,文件中每行包含 WuauservW32Time 字符串,如图 3-1 所示。

Image

图 3-1:一个 Services.txt 文件,其中WuauservW32Time分别列在不同的行上

该文件包含了你想要启动的服务列表。为了简化起见,我这里使用了两个服务,但你可以根据需要添加更多服务。要将文件内容显示到 PowerShell 窗口中,请使用 Get-Content cmdlet 的 Path 参数:

PS> Get-Content -Path C:\Services.txt
Wuauserv
W32Time

Get-Content 命令逐行读取文件,将每一行添加到一个数组中,然后返回该数组。清单 3-3 使用管道将 Get-Content 返回的数组传递给 Get-Service 命令。

PS> Get-Content -Path C:\Services.txt | Get-Service

Status   Name               DisplayName
------   ----               -----------
Stopped  Wuauserv           Windows Update
Stopped  W32Time            Windows Time

清单 3-3:通过管道将 Services.txt 传递给 Get-Service,显示服务列表

Get-Content 命令正在读取文本文件并输出一个数组。但 PowerShell 并没有将整个数组通过管道传递,而是 解开 该数组,将数组中的每一项单独通过管道传递。这样,你就可以为数组中的每一项执行相同的命令。通过将你想要启动的每个服务放入文本文件中,并在清单 3-3 中的命令后面加上一个额外的 | Start-Service,你就能用一个命令启动任意数量的服务。

使用管道连接的命令数量没有限制。但如果你发现自己连接了五个以上的命令,可能需要重新考虑你的方法。请注意,尽管管道功能强大,但并不是所有地方都能使用:大多数 PowerShell 命令仅接受某些类型的管道输入,某些命令甚至根本不接受任何输入。在下一节中,你将深入了解 PowerShell 如何处理管道输入,重点讲解参数绑定。

查看参数绑定

当你将参数传递给命令时,PowerShell 会启动一个叫做 参数绑定 的过程,其中它将你传递给命令的每个对象与命令创建者指定的各种参数进行匹配。为了让 PowerShell 命令接受管道输入,编写该命令的人——无论是 Microsoft 还是你——必须显式地为一个或多个参数构建管道支持。如果你尝试将信息传递到一个没有管道支持的命令中,或者 PowerShell 找不到合适的绑定,就会出现错误。例如,尝试运行以下命令:

PS> 'string' | Get-Process 
Get-Process : The input object cannot be bound to any parameters for the command either...
--snip--

你应该会看到命令不接受管道输入。为了查看是否可以使用管道,你可以使用 Get-Help 命令并带上 Full 参数来查看命令的完整帮助内容。我们使用 Get-Help 查看你在 示例 3-1 中使用的 Get-Service 命令:

PS> Get-Help -Name Get-Service –Full

你应该会看到相当多的输出。向下滚动到 PARAMETERS 部分。该部分列出了每个参数的信息,并提供比不使用 DetailedFull 参数时更多的信息。示例 3-4 显示了 Get-ServiceName 参数的信息。

-Name <string[]>
        Required?                    false
        Position?                    0
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Parameter set name           Default
        Aliases                      ServiceName
        Dynamic?                     false

示例 3-4: Get-Service 命令的 Name 参数信息

这里有很多信息,但我们想要关注的是 Accept pipeline input? 字段。正如你所想象的,这个字段告诉你一个参数是否接受管道输入;如果参数不接受管道输入,你会在此字段旁看到 false。但是注意这里有更多的信息:这个参数同时通过 ByValueByPropertyName 接受管道输入。与此对比的是 ComputerName 参数,它的相关信息在 示例 3-5 中。

-ComputerName <string[]>
        Required?                    false
        Position?                    Named
        Accept pipeline input?       true (ByPropertyName)
        Parameter set name           (all)
        Aliases                      Cn
        Dynamic?                     false

示例 3-5: Get-Service 命令的 ComputerName 参数信息

ComputerName 参数允许你指定希望在哪台计算机上运行 Get-Service。请注意,这个参数也接受 string 类型。那么当你执行类似以下操作时,PowerShell 怎么知道你指的是服务名称,而不是计算机名称呢?

PS> 'wuauserv' | Get-Service

PowerShell 匹配管道输入到参数有两种方式。第一种是通过 ByValue,这意味着 PowerShell 会查看传入对象的类型并相应地解释它。因为 Get-Service 指定它通过 ByValue 接受 Name 参数,所以除非另行指定,否则它会将传入的任何字符串解释为 Name。由于通过 ByValue 传递的参数依赖于输入的类型,每个通过 ByValue 传递的参数只能是一个类型。

PowerShell 匹配管道参数的第二种方式是通过 ByPropertyName。在这种情况下,PowerShell 会查看传入的对象,如果它有一个具有适当名称(在这种情况下是 ComputerName)的属性,则会查看该属性的值并将该值接受为参数。因此,如果你想将服务名称和计算机名称都传递给 Get-Service,你可以创建一个 PSCustomObject 并将其传入,如 示例 3-6 所示。

PS> $serviceObject = [PSCustomObject]@{Name = 'wuauserv'; ComputerName = 'SERV1'}
PS> $serviceObject | Get-Service

示例 3-6: 将自定义对象传递给 Get-Service

通过查看命令的参数规范,并使用哈希表干净地存储所需的参数,你将能够使用管道将各种命令串联起来。但随着你开始编写更复杂的 PowerShell 代码,你将需要比管道更多的东西。在下一节中,你将学习如何将 PowerShell 代码作为脚本外部存储。

编写脚本

脚本 是存储一系列命令的外部文件,你可以通过在 PowerShell 控制台中输入一行命令来运行它们。正如在示例 3-7 中所示,要运行脚本,你只需在控制台中输入脚本的路径。

PS> C:\FolderPathToScript\script.ps1
Hello, I am in a script!

示例 3-7:从控制台运行脚本

虽然在脚本中你能做的事情与在控制台中做的没有区别,但使用脚本运行单个命令比键入几千个命令要容易得多!更不用说,如果你想修改代码中的某些内容,或者你犯了错误,你将需要重新输入那些命令。正如你在本书后面将看到的,脚本可以让你编写复杂、健壮的代码。但在你开始编写脚本之前,你需要更改一些 PowerShell 设置,以确保你可以运行它们。

设置执行策略

默认情况下,PowerShell 不允许你运行任何脚本。如果你尝试在默认的 PowerShell 安装中运行外部脚本,你会遇到示例 3-8 中的错误。

PS> C:\PowerShellScript.ps1 
C:\PowerShellScript.ps1: File C:\PowerShellScript.ps1 cannot be loaded because
running scripts is disabled on this system. For more information, see about
_Execution_Policies at http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ C:\PowerShellScript.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

示例 3-8:尝试运行脚本时发生的错误

这个令人沮丧的错误信息是 PowerShell 的执行策略造成的,这是一个安全措施,用来决定哪些脚本可以运行。执行策略有四种主要配置:

Restricted 该配置是默认配置,不允许你运行脚本。

AllSigned 该配置仅允许你运行那些经过受信任方加密签名的脚本(稍后会详细介绍)。

RemoteSigned 该配置允许你运行任何自己编写的脚本,以及任何你下载的脚本,只要它们经过受信任方的加密签名。

Unrestricted 该配置允许你运行任何脚本。

要查看你的计算机当前使用的执行策略,可以运行示例 3-9 中的命令。

PS> Get-ExecutionPolicy
Restricted

示例 3-9:使用 Get-ExecutionPolicy 命令显示当前的执行策略

当你运行此命令时,很可能会得到 Restricted。为了本书的目的,你将把执行策略更改为 RemoteSigned。这将允许你运行任何你编写的脚本,同时确保你仅使用来自受信任来源的外部脚本。要更改执行策略,请使用 Set-ExecutionPolicy 命令并传入你想要的策略,如 Listing 3-10 所示。请注意,你需要以管理员身份运行此命令(有关以管理员身份运行命令的更多信息,请参见 第一章)。你只需要执行一次此命令,因为设置会保存在注册表中。如果你在一个大型的 Active Directory 环境中,还可以通过组策略将执行策略设置到多个计算机上。

PS> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. Changing the
execution policy might expose you to the security risks described in the about_Execution
_Policies help topic at http://go.microsoft.com/fwlink/?LinkID=135170\. Do you want to change
the execution policy?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): A

Listing 3-10:使用 Set-ExecutionPolicy 命令更改执行策略

再次运行 Get-ExecutionPolicy 命令,以验证你是否成功将策略更改为 RemoteSigned。如前所述,你不需要每次打开 PowerShell 时都设置执行策略。策略将保持在 RemoteSigned,直到你决定再次更改它。

脚本签名

脚本签名 是附加在脚本末尾的加密字符串,作为注释存在;这些签名是通过安装在你电脑上的证书生成的。当你将策略设置为 AllSignedRemoteSigned 时,你将只能运行那些正确签名的脚本。签名源代码让 PowerShell 知道脚本的来源是可靠的,并且脚本作者确实如他们所说的那样。一个脚本签名看起来大致如下:

# SIG # Begin signature block
# MIIEMwYJKoZIhvcNAQcCoIIEJDCCBCACAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU6vQAn5sf2qIxQqwWUDwTZnJj
--snip--
# m5ugggI9MIICOTCCAaagAwIBAgIQyLeyGZcGA4ZOGqK7VF45GDAJBgUrDgMCHQUA
# Dxoj+2keS9sRR6XPl/ASs68LeF8o9cM=
# SIG # End signature block

你应该签名任何你在专业环境中创建和执行的脚本。我在这里不会详细讲解如何做,但我找到的最好的资源之一是 Carlos Perez(著名安全专家)写的文章系列《PowerShell 基础——执行策略和代码签名》,你可以在 www.darkoperator.com/blog/2013/3/5/powershell-basics-execution-policy-part-1.html 中找到。

PowerShell 脚本编写

现在,执行策略已设置完成,是时候编写脚本并在控制台中执行它了。你可以在任何文本编辑器中编写 PowerShell 脚本(如 Emacs、Vim、Sublime Text、Atom,甚至 Notepad),但编写 PowerShell 脚本的最便捷方式是使用 PowerShell 集成脚本环境(ISE)或 Microsoft 的 Visual Studio Code 编辑器。从技术上讲,ISE 已被弃用,但它随 Windows 一起预安装,因此它可能是你最先发现的编辑器。

使用 PowerShell ISE

要启动 PowerShell ISE,运行 Listing 3-11 中的命令。

PS> powershell_ise.exe

Listing 3-11:打开 PowerShell ISE

一个看起来像 图 3-2 的交互式控制台界面应该会打开。

图片

图 3-2:PowerShell ISE 界面

要添加脚本,点击 文件 ▶ 新建。屏幕应分为两部分,控制台上方会出现一个白色面板,如图 3-3 所示。

图片

图 3-3:打开脚本的 PowerShell ISE 界面

点击 文件 ▶ 保存,将新文件保存为 WriteHostExample.ps1。我将我的脚本保存在 C: 盘的根目录下,所以它的位置是 C:\WriteHostExample.ps1。注意,你需要以 .ps1 扩展名保存脚本;这个扩展名告诉你的系统该文件是一个 PowerShell 脚本。

你将在白色面板中输入脚本的所有文本。PowerShell ISE 允许你在同一个窗口中编辑并运行脚本,这样在编辑时可以节省大量繁琐的反复操作。PowerShell ISE 还有许多其他功能,尽管我在这里不会介绍它们。

PowerShell 脚本是简单的文本文件。你使用哪个文本编辑器并不重要,只要使用正确的 PowerShell 语法即可。

编写你的第一个脚本

使用你喜欢的编辑器,将 列表 3-12 中的那一行添加到你的脚本中。

Write-Host 'Hello, I am in a script!'

列表 3-12:脚本中的第一行

请注意,行首没有 PS>。从现在开始,这就是你判断我们是在控制台中操作还是在编写脚本的方式。

要运行此脚本,请前往控制台并键入脚本的路径,如 列表 3-13 所示。

PS> C:\WriteHostExample.ps1
Hello, I am in a script!

列表 3-13:在控制台中执行 WriteHostExample.ps1 *

在这里,你使用完整路径运行 WriteHostExample.ps1。如果你已经位于包含要运行脚本的目录中,可以使用点符号表示当前工作目录,如:.\WriteHostExample.ps1

恭喜,这就完成了——你已经创建了第一个脚本!它可能看起来不算什么,但这是朝正确方向迈出的重要一步。在本书结束时,你将能够在脚本中定义自己的 PowerShell 模块,甚至包含数百行代码。

总结

在本章中,你学习了两种将命令组合在一起的有价值的方法:管道和脚本。你还了解了如何更改执行策略,并通过查看参数绑定来揭示管道背后的某些奥秘。我们为创建更强大的脚本打下了基础,但在我们进入下一步之前,还需要涵盖一些关键概念。在第四章中,你将学习如何通过使用控制流结构,如 if/then 语句和 for 循环,使代码更加健壮。

第四章:控制流

Images

让我们快速回顾一下。在第三章中,你学习了如何通过使用管道和外部脚本来组合命令。在第二章中,你学习了如何使用变量来存储值。使用变量的一个主要好处是,它们让你能够编写处理值意义的代码:例如,不是直接处理数字 3,而是处理更通用的$serverCount,这样你就可以编写出无论有一台、两台还是一千台服务器都能运行的代码。将这种编写通用解决方案的能力与将代码存储在脚本中的能力相结合,使你能够在多台计算机上运行,从而开始解决更大规模的问题。

但是在现实世界中,有时候是否使用一台服务器、两台服务器或一千台服务器是很重要的。现在,你没有一个很好的方式来处理这个问题:你的脚本是单向执行的——从上到下——并且它们没有办法根据你正在处理的具体值进行改变。在本章中,你将使用控制流和条件逻辑来编写根据所处理的值执行不同指令序列的脚本。到本章结束时,你将学会如何使用if/then语句、switch语句以及各种循环语句,为你的代码提供所需的灵活性。

理解控制流

你将编写一个脚本,用来读取存储在多台远程计算机中的文件内容。为了跟上步骤,请从本书的资源下载一个名为App_configuration.txt的文件,网址是https://github.com/adbertram/PowerShellForSysadmins/,并将其放置在几台远程计算机的*C:*驱动器根目录下。(如果你没有远程服务器的访问权限,现在可以仅通过文本跟随。)在这个示例中,我将使用名为SRV1SRV2SRV3SRV4SRV5的服务器。

要访问文件内容,你将使用Get-Content命令,并将文件的路径作为Path参数的参数提供,如下所示:

Get-Content -Path "\\servername\c$\App_configuration.txt"

作为第一次尝试,让我们把所有的服务器名称存储在一个数组中,并对数组中的每台服务器运行此命令。打开一个新的.ps1文件,并在 Listing 4-1 中输入代码。

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')
Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[2])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[3])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[4])\c$\App_configuration.txt"

Listing 4-1:获取多台服务器上文件的内容

从理论上讲,这段代码应该没有问题。但是这个示例假设你的环境中一切都很完美。如果SRV2宕机了怎么办?如果有人忘了把App_configuration.txt文件移动到SRV4上呢?或者用了不同的文件路径?你可以为每台服务器编写不同的脚本,但这种解决方案无法扩展——尤其是在你开始添加越来越多的服务器时。你需要的是可以根据遇到的情况执行不同的代码。

这就是控制流的基本概念,即根据预定的逻辑,让你的代码执行不同的指令序列。你可以将脚本看作沿着某一特定路径执行。目前,这条路径从代码的第一行直接走到最后一行,但你可以使用控制流语句在道路上增加分支、回到已经走过的地方,或者跳过某些行。通过为脚本引入不同的执行路径,你可以让脚本更加灵活,从而编写一个能够应对多种情况的脚本。

你将从最基本的控制流类型开始:条件语句。

使用条件语句

在第二章中,你了解了布尔值的概念:一个真假值。你可以使用布尔值来构建条件语句,该语句根据表达式(称为条件)的值是True还是False来决定是否执行特定的代码块。条件就像一个是/否问题:你有超过五台服务器吗?服务器 3 是否在线?这个文件路径是否存在?要开始使用条件语句,让我们看看如何将这些问题转化为表达式。

使用运算符构建表达式

你可以使用比较运算符来编写表达式,这些运算符用来比较值。使用比较运算符时,将它放在两个值之间,如下所示:

PS> 1 –eq 1
True

你使用–eq运算符来判断两个值是否相等。以下是你将使用的最常见比较运算符列表:

-eq 比较两个值,如果它们相等,则返回True

-ne 比较两个值,如果它们不相等,则返回True

-gt 比较两个值,如果第一个值大于第二个,则返回True

-ge 比较两个值,如果第一个值大于或等于第二个,则返回True

-lt 比较两个值,如果第一个值小于第二个,则返回True

-le 比较两个值,如果第一个值小于或等于第二个,则返回True

-contains 如果第二个值“在”第一个值中,则返回True。你可以使用它来确定一个值是否在数组中。

PowerShell 提供了更高级的比较运算符。我现在不会详细介绍它们,但我建议你查阅微软文档中的关于比较运算符/,或者在 PowerShell 帮助文档中查看(见第一章)。

你可以使用前面提到的操作符来比较变量和值。但一个表达式不一定非得是比较。有时 PowerShell 命令本身就可以作为条件。在之前的例子中,你想知道服务器是否在线。你可以通过使用 Test-Connection cmdlet 来测试服务器是否可以被 ping 通。通常,Test-Connection 的输出会返回一个充满信息的对象,但通过使用 Quiet 参数,你可以强制命令仅返回简单的 TrueFalse,并通过 Count 参数限制测试次数为一次。

PS> Test-Connection -ComputerName offlineserver -Quiet -Count 1
False

PS> Test-Connection -ComputerName onlineserver -Quiet -Count 1
True

如果你想知道服务器是否离线,可以使用 –not 操作符将表达式转换为其相反的值:

PS> -not (Test-Connection -ComputerName offlineserver -Quiet -Count 1)
True

现在你已经了解了表达式的基础,接下来我们来看最简单的条件语句。

if 语句

if 语句很简单:如果 X 为真,则执行 Y。就是这么简单!

编写 if 语句时,你首先写 if 关键字,后面跟括号包含的条件。表达式之后是一个代码块,用大括号括起来。PowerShell 只有在表达式评估为 True 时才会执行该代码块。如果 if 表达式评估为 False 或根本没有返回任何内容,则跳过该代码块。你可以在清单 4-2 中看到基本的 if/then 语句的语法。

if (condition) {
    # code to run if the condition evaluates to be True
}

清单 4-2:if 语句的语法

这个例子使用了一些新的语法:井号 (#) 表示 注释,这段文本 PowerShell 会忽略。你可以使用注释来给自己或任何阅读代码的人留下有用的备注和说明。

现在让我们再看一遍清单 4-1 中的代码,看看如何使用 if 语句确保你不会尝试访问一个无法连接的服务器。在上一节中,你已经看到如何将 Test-Connection 用作返回 TrueFalse 的表达式,所以接下来我们将把 Test-Connection 命令包装在一个 if 语句中,并使用 Get-Content 来避免访问无法连接的服务器。暂时只更改第一个服务器的代码,如清单 4-3 所示。

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')
if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {
    Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"
}
Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"
--snip--

清单 4-3:使用 if 语句选择性地获取服务器内容

由于你将 Get-Content 放入了 if 语句中,因此如果你尝试访问一个死掉的服务器,也不会遇到任何错误;如果测试失败,脚本会知道不再尝试读取文件。只有在你已经知道服务器在线时,才会尝试访问它。但请注意,这段代码只处理条件为真的情况。通常情况下,你可能希望在条件为真时有一种行为,在条件为假时有另一种行为。接下来的部分,你将看到如何使用 else 语句来指定假条件的行为。

else 语句

要为你的 if 语句添加备用行为,你需要在 if 语句块的右括号后面使用 else 关键字,然后再加上一对大括号,里面放置一个代码块。如 列表 4-4 所示,当第一个服务器没有响应时,使用 else 语句将错误返回到控制台。

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {
    Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"
} else {
    Write-Error -Message "The server $($servers[0]) is not responding!"
}

列表 4-4:使用 else 语句在条件不成立时运行代码

当你有两个互斥的情况时,if/else 语句非常有效。在这里,服务器要么在线,要么不在线;你只需要两条代码分支。让我们来看一下如何处理更复杂的情况。

elseif 语句

else 语句就像一个兜底语句:如果第一个 if 语句失败,不管怎样都会执行。对于一个二元条件,比如服务器是否在线,这个方法很有效。但有时你需要考虑更多的变数。例如,假设你有一台服务器,你知道它没有你想要的文件,而且你已经把这台服务器的名称存储在变量 $problemServer 中(这行代码需要你自己加到脚本中!)。这意味着你需要额外的检查,看看你正在处理的服务器是否是问题服务器。你可以通过使用嵌套的 if 语句来考虑这个情况,如下面的代码所示:

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {
    if ($servers[0] –eq $problemServer) {
        Write-Error -Message "The server $servers[0] does not have the right file!"
    } else {
        Get-Content -Path "\\$servers[0]\c$\App_configuration.txt"
    }
} else {
    Write-Error -Message "The server $servers[0] is not responding!"
}
--snip--

但是,更简洁的方式是使用 elseif 语句,它允许你在回退到 else 代码块之前添加额外的条件检查。elseif 语句的语法与 if 语句完全相同。因此,要使用 elseif 语句检查问题服务器,可以参考 列表 4-5 中的代码。

if (-not (Test-Connection -ComputerName $servers[0] -Quiet -Count 1)) { ❶
    Write-Error -Message "The server $servers[0] is not responding!"
} elseif ($servers[0] –eq $problemServer) { ❷
    Write-Error -Message "The server $servers[0] does not have the right file!"
} else {
    Get-Content -Path "\\$servers[0]\c$\App_configuration.txt" ❸
} 
--snip--

列表 4-5:使用 elseif 语句块

请注意,你不仅仅是添加了一个 elseif;你还改变了逻辑。现在你通过先使用 –not 运算符 ❶ 来检查服务器是否离线。然后,一旦确定服务器是否在线,你就检查它是否是问题服务器 ❷。如果不是,你就使用 else 语句执行默认行为——获取文件 ❸。如你所见,像这样的代码有多种结构方式。重要的是代码能够正常工作,并且对别人(无论是同事第一次阅读,还是你回头查看一段时间前写的脚本)来说具有可读性。

你可以根据需要将多个 elseif 语句串联起来,这样就能处理更多的情况。然而,elseif 语句是互斥的:当某个 elseif 语句的条件为 True 时,PowerShell 只会执行该语句块中的代码,而不会测试其他情况。在 列表 4-5 中,这并没有引发问题,因为你只需要在检查服务器是否在线之后,测试是否是问题服务器,但这点在以后编写代码时需要牢记。

ifelseelseif 语句非常适合处理简单的是/否问题。在下一部分,你将学习如何处理稍微复杂一点的逻辑。

switch 语句

让我们稍微调整一下我们的例子。假设你有五台服务器,每台服务器的文件路径不同。根据你现在所知道的,你需要为每个服务器写一个单独的 elseif 语句。这样是可以的,但有一种更简洁的方法。

请注意,现在你正在处理一种不同类型的条件。之前你想要的是对是/否问题的答案,而在这里,你想知道的是某个东西的具体值:服务器是 SRV1 吗?是 SRV2 吗?以此类推。如果你只处理一个或两个特定值,使用 if 就可以,但在这种情况下,使用 switch 语句会更简洁。

switch 语句允许你根据某个值执行不同的代码块。它由 switch 关键字和括号内的表达式组成。在 switch 块内有一系列的语句,每个语句后跟一个包含代码块的大括号,并最终有一个 default 块,像列表 4-6 那样。

switch (expression) {
    expressionvalue {
        # Do something with code here.
    }
    expressionvalue {
    }
    default {
        # Stuff to do if no matches were found
    }
}

列表 4-6:switch 语句模板

switch 语句可以包含(几乎)无限多个值。如果表达式计算出一个值,则执行该值块内的代码。关键是,与 elseif 不同,PowerShell 在执行完一个代码块后会继续评估其他条件,除非另有指定。如果没有任何值与计算出的值匹配,PowerShell 将执行 default 关键字下的嵌入代码。为了强制 PowerShell 停止评估 switch 语句中的条件,可以在代码块末尾使用 break 关键字,像列表 4-7 那样。

switch (expression) {
    expressionvalue {
        # Do something with code here.
        break
    }
--snip--

列表 4-7:在 switch 语句中使用 break 关键字

break 关键字可以用来使你的 switch 条件互斥。让我们回到我们五台服务器的例子,这些服务器的文件路径不同。你知道你正在处理的服务器只能有一个值(它不可能同时是 SRV1SRV2),因此你必须使用 break 语句。你的脚本应该像列表 4-8 那样。

$currentServer = $servers[0]
switch ($currentServer) {
    $servers[0] {
        # Check if server is online and get content at SRV1 path.
        break
    }
    $servers[1] {
        ## Check if server is online and get content at SRV2 path.
        break
    }

    $servers[2] {
 ## Check if server is online and get content at SRV3 path.
        break
    }
--snip--

列表 4-8:使用 switch 语句检查不同的服务器

你可以通过仅使用 ifelseif 语句来重写这段代码(我鼓励你尝试一下!)。但无论你如何编写,它都会要求你为列表中的每台服务器重复相同的结构,这意味着你的脚本将变得相当长——而且如果你想要测试 500 台服务器而不是 5 台服务器,想想看会是什么样子。在下一部分,你将学习如何通过使用最基本的控制流结构之一——循环,来避免这种麻烦。

使用循环

计算机工作中的一个好法则:不要重复自己(DRY)。如果你发现自己做同样的事情超过一次,很可能有办法自动化它。编写代码也是如此:如果你反复使用相同的代码行,很可能有更好的解决方案。

避免重复代码的一种方法是使用循环。循环允许你反复执行代码,直到条件发生变化。停止条件可以用于使循环执行设定次数,直到布尔值发生变化,甚至使循环无限执行。我们称每次运行循环为一次迭代

PowerShell 提供了五种类型的循环:foreachfordo/whiledo/untilwhile。本节解释了每种类型的循环,指出其独特之处,并强调了使用它的最佳场景。

foreach循环

我们将从你在 PowerShell 中最常用的循环类型开始,即foreach循环。foreach循环遍历一个对象列表,并对每个对象执行相同的操作,直到完成最后一个对象为止。这个对象列表通常由数组表示。当你在对象列表上运行循环时,我们称之为迭代该列表。

当你需要对许多不同但相关的对象执行相同任务时,foreach循环非常有用。让我们回到 Listing 4-1(这里复述一下):

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')
Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[2])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[3])\c$\App_configuration.txt"
Get-Content -Path "\\$($servers[4])\c$\App_configuration.txt"

现在你将忽略前面一节中添加的所有复杂逻辑,并将其放入foreach循环中。但与 PowerShell 中的其他循环不同,foreach循环有三种使用方式:作为foreach语句、作为ForEach-Object cmdlet,或作为foreach()方法。虽然每种方式的使用类似,但你应该了解它们的区别。在接下来的三个部分中,你将通过使用每种类型的foreach循环来重写 Listing 4-1。

foreach语句

你将要看的第一个foreach类型是foreach语句。Listing 4-9 展示了 Listing 4-1 的循环版本。

foreach ($server in $servers) {
    Get-Content -Path "\\$server\c$\App_configuration.txt"
}

Listing 4-9: 使用foreach语句

如你所见,foreach语句后面跟着包含三个元素的括号,顺序是:一个变量、关键字in,以及要迭代的对象或数组。你提供的变量可以有任何名称,但我建议尽可能保持名称具有描述性。

当 PowerShell 遍历列表时,它会将正在查看的对象复制到变量中。请注意,由于变量只是副本,你不能直接更改原始列表中的项目。为了验证这一点,试试运行以下代码:

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')
foreach ($server in $servers) {
    $server = "new $server"
}
$servers

你应该得到像这样的结果:

SRV1
SRV2
SRV3
SRV4
SRV5

什么都没改变!这是因为你只是在修改数组中原始变量的副本。这是使用foreach循环(任何类型)的一大缺点。要直接修改你正在遍历的列表的原始内容,你必须使用其他类型的循环。

ForEach-Object cmdlet

foreach 语句一样,ForEach-Object cmdlet 可以遍历一组对象并执行某个操作。但因为 ForEach-Object 是一个 cmdlet,所以你必须将那组对象和完成的操作作为参数传递。

查看清单 4-10,看看如何使用 ForEach-Object cmdlet 完成与清单 4-9 相同的操作。

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')
ForEach-Object -InputObject $servers -Process {
    Get-Content -Path "\\$_\c$\App_configuration.txt"
}

清单 4-10:使用 ForEach-Object cmdlet

这里有一些不同,让我们一起来看一下。注意,ForEach-Object cmdlet 接受一个 InputObject 参数。在这个例子中,你使用的是 $servers 数组,但你可以使用任何对象,比如字符串或整数。在这些情况下,PowerShell 将只执行一次迭代。该 cmdlet 还接受一个 Process 参数,该参数应该是一个包含你希望在每个输入对象内元素上运行的代码的脚本块。(脚本块 是你传递给 cmdlet 作为一个整体的语句集合。)

你可能已经注意到清单 4-10 中另一个奇怪的地方。与使用 foreach 语句时使用 $server 变量不同,这里你使用了语法 $_。这种特殊语法表示管道中的当前对象。foreach 语句和 ForEach-Object cmdlet 之间的主要区别在于,cmdlet 接受管道输入。实际上,ForEach-Object 几乎总是通过管道传入 InputObject 参数来使用,如下所示:

$servers | ForEach-Object -Process {
    Get-Content -Path "\\$_\c$\App_configuration.txt"
}

ForEach-Object cmdlet 可以节省大量时间。

foreach() 方法

你将要查看的最后一种 foreach 循环是 PowerShell V4 引入的 foreach() 对象方法。foreach() 方法在 PowerShell 中所有数组上都存在,可以用来完成与 foreachForEach-Object 相同的操作。foreach() 方法接受一个脚本块参数,该参数应该包含要执行每次迭代的代码。与 ForEach-Object 一样,你可以使用 $_ 来捕捉当前迭代的对象,正如你在清单 4-11 中看到的那样。

$servers.foreach({Get-Content -Path "\\$_\c$\App_configuration.txt"})

清单 4-11:使用 foreach() 方法

foreach() 方法比其他两种方法快得多,尤其在处理大型数据集时,差异尤为明显。我建议在可能的情况下优先使用这种方法。

foreach 循环非常适合你希望对每个对象逐个执行任务的情况。但如果你想做一些更简单的事情呢?如果你希望执行某个任务一定次数该怎么办?

for 循环

要执行预定次数的代码,你可以使用 for 循环。清单 4-12 显示了基本 for 循环的语法。

for (❶$i = 0; ❷$i -lt 10; ❸$i++) {
    ❹ $i
}

清单 4-12:一个简单的 for 循环

for 循环由四个部分组成:迭代变量声明 ❶,继续执行循环的条件 ❷,每次成功循环后对迭代变量进行的操作 ❸,以及你想要执行的代码 ❹。在这个例子中,你从将变量 $i 初始化为 0 开始。然后,你检查 $i 是否小于 10;如果是,就执行花括号中的代码,这会打印 $ix。代码执行后,你将 $i 增加 1 ❸,并检查它是否仍然小于 10 ❷。你重复这个过程,直到 $i 不再小于 10,从而完成 10 次迭代。

for 循环可以这样使用,执行某个任务任意次数——只需替换条件 ❷ 来满足你的需求。但 for 循环有更多的用途。最强大的用途之一是操作数组中的元素。之前,你已经看到如何不能使用 foreach 循环来修改数组中的元素。让我们再试一次,使用 for 循环:

$servers = @('SERVER1','SERVER2','SERVER3','SERVER4','SERVER5')
for ($i = 0; $i –lt $servers.Length; $i++) {
    $servers[$i] = "new $($servers[$i])"
}
$servers

尝试运行这个脚本。服务器名称应该会发生变化。

for 循环在执行需要多个数组元素的操作时也特别有用。例如,假设你的 $servers 数组按特定顺序排列,你想知道哪个服务器在另一个服务器之后。为此,你可以使用 for 循环:

for (❶$i = 1; $i –lt $servers.Length; $i++) {
    Write-Host $servers[$i] "comes after" $servers[$i-1]
}

请注意,这次你将迭代变量声明为从 1 开始 ❶。这确保你不会尝试访问第一个服务器之前的服务器,否则会导致错误。

正如你将在本书中看到的那样,for 循环是一个强大的工具,除了这里提供的简单示例外,它还有许多其他用途。现在,让我们继续讨论下一种类型的循环。

while 循环

while 循环是最简单的循环:只要条件为真,就执行某个操作。为了理解 while 循环的语法,让我们将 Listing 4-12 中的 for 循环重写为 Listing 4-13 所示。

$counter = 0
while ($counter -lt 10) {
    $counter
    $counter++
}

Listing 4-13: 使用 while 循环的简单计数器

如你所见,要使用 while 循环,只需将你想评估的条件放入括号内,将你想运行的代码放入花括号中。

while 循环最适合用于循环次数预先确定的情况。例如,假设你有一台频繁宕机的 Windows 服务器(再次称为 $problemServer)。但你需要从中获取一个文件,而不想每隔几分钟就去测试服务器是否正常。你可以使用 while 循环来自动化这个过程,如 Listing 4-14 所示。

while (Test-Connection -ComputerName $problemServer -Quiet -Count 1) {
    Get-Content -Path "\\$problemServer\c$\App_configuration.txt"  
    break
}

Listing 4-14: 使用 while 循环处理有问题的服务器

通过使用 while 循环代替 if,你可以反复检查服务是否正常运行。然后,一旦获取到所需的内容,你可以使用 break 跳出循环,确保不继续检查服务器。break 关键字可以在任何循环中使用,用来停止循环的执行。这在使用最常见的 while 循环之一时尤其重要:while($true) 循环。通过使用 $true 作为条件,除非你通过 break 或键盘输入停止它,否则 while 循环会永远运行下去。

do/whiledo/until 循环

while 循环类似,do/whiledo/until 循环也是非常相似的。二者本质上是反向的:do/while 循环在条件为真时执行某个操作,而 do/until 循环则在条件为真时停止执行某个操作。

一个空的 do/while 循环看起来像这样:

do {
    } while ($true)

如你所见,do 代码位于 while 条件之前。while 循环和 do/while 循环之间的主要区别在于,do/while 循环会在条件评估之前先执行代码。

在某些情况下,这可能非常有用,特别是当你不断从一个来源接收输入并想要对其进行评估时。例如,假设你想提示用户询问他们最喜欢的编程语言。为了做到这一点,你可以使用 Listing 4-15 中的代码。在这里,你将使用 do/until 循环:

do {
    $choice = Read-Host -Prompt 'What is the best programming language?'
} until ($choice -eq 'PowerShell')
Write-Host -Object 'Correct!'

Listing 4-15: 使用 do/until 循环

do/whiledo/until 循环非常相似。通常,这意味着你可以通过简单地反转条件,使用每种循环来完成相同的事情,正如你在这里所做的那样。

摘要

我们在本章中讲了很多内容。你学习了控制流,了解了如何使用条件逻辑在代码中引入不同的路径。你看到了各种类型的控制流语句,包括 if 语句、switch 语句,以及 foreachforwhile 循环。最后,你通过使用 PowerShell 检查服务器是否正常运行并访问服务器上的文件,获得了一些实践经验。

你可以使用条件逻辑来处理一些错误,但很可能会遗漏一些内容。在 第五章 中,你将更深入地了解错误以及一些处理错误的技巧。

第五章:错误处理

Images

你已经看到如何使用变量和控制流结构编写灵活的代码,以应对现实世界中的不完美——比如服务器未按预期启动、文件放错了位置等。有些情况是你预料到的,并且可以相应地处理。但你永远无法预测每一个错误,总有些东西会导致代码崩溃。你能做的最好的事情,就是编写能够负责任地崩溃的代码。

这就是错误处理的基本前提,开发者使用这些技术确保他们的代码能预见并处理——或者处理——错误。在本章中,你将学习一些最基本的错误处理技术。首先,你将深入了解错误本身,看看终止性错误和非终止性错误的区别。然后,你将学习如何使用try/catch/finally结构,最后,你将了解 PowerShell 的自动错误变量。

处理异常和错误

在第四章中,你已经了解了控制流以及如何将不同的执行路径引入你的代码。当你的代码遇到问题时,它会打乱正常的执行流程;我们将这种打乱流程的事件称为异常。像除以零、尝试访问数组范围外的元素或尝试打开缺失的文件等错误,都会导致 PowerShell抛出异常。

一旦异常被抛出,如果你什么都不做来阻止它,它将被附加额外信息,并作为错误发送给用户。PowerShell 有两种类型的错误。第一种是终止性错误:任何会停止代码执行的错误。例如,假设你有一个名为Get-Files.ps1的脚本,它查找某个文件夹中的文件列表,然后对这些文件执行相同的操作。如果脚本找不到该文件夹——有人将其移动或重命名——你会希望返回一个终止性错误,因为代码在没有访问所有文件的情况下无法执行。但如果只有其中一个文件损坏了,怎么办呢?

当你尝试访问损坏的文件时,你会遇到另一个异常。但是因为你在每个文件上执行的是相同的独立操作,所以没有理由让一个损坏的文件阻止其余文件的运行。在这种情况下,你会编写代码,将由单个损坏文件引发的异常视为非终止性错误,即错误并不严重到足以停止其余代码的执行。

非终止性错误的一般错误处理行为是输出有用的错误信息,并继续执行程序的其余部分。你可以在 PowerShell 的几个内置命令中看到这一点。例如,假设你想检查 Windows 服务bitsfoolanmanserver的状态。你可以使用一个Get-Service命令同时检查它们,如清单 5-1 所示。

PS> Get-Service bits,foo,lanmanserver
Get-Service : Cannot find any service with service name 'foo'.
At line:1 char:1
+ Get-Service bits,foo,lanmanserver
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (foo:String) [Get-Service], ServiceCommandException
+ FullyQualifiedErrorId : NoServiceFoundForGivenName,
                          Microsoft.PowerShell.Commands.GetServiceCommand

Status   Name               DisplayName
------   ----               -----------
Running  bits               Background Intelligent Transfer Ser...
Running  lanmanserver       Server

清单 5-1:一个非终止性错误

当然,根本没有foo服务,PowerShell 也会告诉你这一点。但请注意,PowerShell 会获取其他服务的状态;它在遇到这个错误时并不会停止执行。这个非终止性错误可以转换为终止性错误,以防止代码的其余部分执行。

重要的是要理解,决定将异常转为非终止性错误还是终止性错误是由开发者做出的。通常,如清单 5-1 中所示,这个决定是由编写你使用的 cmdlet 的人做出的。在许多情况下,如果 cmdlet 遇到异常,它会返回一个非终止性错误,将错误输出写入控制台,并允许你的脚本继续执行。在下一节中,你将看到几种将非终止性错误转换为终止性错误的方法。

处理非终止性错误

假设你想编写一个简单的脚本,进入一个你知道包含多个文本文件的文件夹,并打印出每个文本文件的第一行。如果文件夹不存在,你希望脚本立即结束并报告错误;否则,如果遇到其他任何错误,你希望脚本继续运行并报告错误。

你将开始编写一个脚本,应该返回一个终止性错误。清单 5-2 展示了这个代码的首次尝试。(虽然我本可以将代码简化为更简洁的形式,但为了教学目的,我尽力让每个步骤尽可能清晰。)

$folderPath = '.\bogusFolder'
$files = Get-ChildItem -Path $folderPath 
Write-Host "This shouldn't run."
$files.foreach({
    $fileText = Get-Content $files
    $fileText[0]
})

清单 5-2:我们 Get-Files.ps1 脚本的首次尝试

在这里,你使用Get-ChildItem返回路径中包含的所有文件——在这种情况下,是一个虚假的文件夹。如果你运行这个脚本,你应该得到如下输出:

Get-ChildItem : Cannot find path 'C:\bogusFolder' because it does not exist.
At C:\Get-Files.ps1:2 char:10
+ $files = Get-ChildItem -Path $folderPath
+          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\bogusFolder:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand
This shouldn't run.

如你所见,会发生两件事:PowerShell 返回一个错误,指定遇到的异常类型(ItemNotFoundException),并且调用Write-Host也会运行。这意味着你得到的错误是非终止性的。

要将此错误转化为终止性错误,你将使用ErrorAction参数。这是一个常见参数,意味着它内置于每个 PowerShell cmdlet 中。ErrorAction参数决定了当 cmdlet 遇到非终止性错误时应该采取什么行动。该参数有五个主要选项:

继续 输出错误消息并继续执行 cmdlet。这是默认值。

忽略 继续执行 cmdlet,且不输出错误或将其记录在$Error变量中。

询问 输出错误消息并提示用户输入,然后继续执行。

静默继续 继续执行 cmdlet 而不输出错误,但将其记录在$Error变量中。

停止 输出错误消息并停止 cmdlet 的执行。

你将在本章稍后进一步了解$Error变量。现在,你想要将Stop传递给Get-ChildItem。更新你的脚本并再次运行代码。你应该会得到相同的输出,但没有This shouldn't run.(这不该运行)。

ErrorAction参数对于按情况控制错误行为非常有用。要更改 PowerShell 如何处理所有非终止性错误,可以使用$ErrorActionPreference变量,这是一个自动变量,用于控制默认的非终止性错误行为。默认情况下,$ErrorActionPreference设置为Continue。请注意,ErrorAction参数会覆盖$ErrorActionPreference的值。

通常来说,我认为最佳实践是始终将$ErrorActionPreference设置为Stop,以彻底去除非终止性错误的概念。这样可以让你捕获所有类型的异常,并避免事先知道哪些错误是终止性的,哪些是非终止性的。你也可以通过在每个命令上使用ErrorAction参数来更细致地定义哪些命令会返回终止性错误,但我更愿意一次性设置规则并忘记它,而不是每次调用命令时都要记得添加ErrorAction参数。

现在让我们看看如何使用try/catch/finally结构来处理终止性错误。

处理终止性错误

为了防止终止性错误停止程序,你需要捕获它们。你可以使用try/catch/finally结构来实现。列表 5-3 展示了语法。

try {
    # initial code
} catch {
    # code that runs if terminating error found
} finally {
    # code that runs at the end
}

列表 5-3:try/catch/finally 结构的语法

使用try/catch/finally基本上设置了一个错误处理的安全网。try块包含你想要运行的原始代码;如果发生终止性错误,PowerShell 会将流程重定向到catch块中的代码。无论catch块中的代码是否运行,finally块中的代码都会始终运行——请注意,finally块是可选的,不像trycatch

为了更好地理解try/catch/finally能做什么和不能做什么,我们来重新审视一下我们的Get-Files.ps1脚本。你将使用try/catch语句来提供更清晰的错误信息,如列表 5-4 所示。

$folderPath = '.\bogusFolder'
try {
    $files = Get-ChildItem -Path $folderPath –ErrorAction Stop
    $files.foreach({
        $fileText = Get-Content $files
        $fileText[0]
    })
} catch {
    $_.Exception.Message
}

列表 5-4:使用 try/catch 语句来处理终止性错误

当捕获到终止错误时,错误对象会存储在$_变量中。在此示例中,你使用$_.Exception.Message来仅返回异常消息。在这种情况下,代码应该返回类似无法找到路径 'C:\ bogusFolder',因为该路径不存在的内容。错误对象还包含其他信息,包括抛出异常的类型、显示异常抛出前代码执行历史的堆栈跟踪等。然而,现在最有用的信息是Message属性,因为它通常包含你需要查看代码发生了什么的基本信息。

到目前为止,你的代码应该按预期工作。通过将Stop传递给ErrorAction,你确保缺少文件夹时将返回终止错误并捕获该错误。但是,如果在尝试使用Get-Content访问文件时遇到错误会发生什么呢?

作为实验,尝试运行以下代码:

$filePath = '.\bogusFile.txt'
try {
    Get-Content $filePath
} catch {
    Write-Host "We found an error"
}

你应该从 PowerShell 收到错误信息,而不是你在catch块中编写的自定义错误。这是因为Get-Content在找不到项时返回的是非终止错误,而try/catch只能捕获终止错误。这意味着清单 5-4 中的代码将按预期工作——任何访问文件本身时出现的错误不会停止程序的执行,而是会返回控制台。

请注意,你在这段代码中没有使用finally块。finally块是执行必要清理任务(例如断开数据库连接、清理 PowerShell 远程会话等)的一好地方。在这里,没有必要进行这样的操作。

探索$Error自动变量

在本章中,你已强制 PowerShell 返回了许多错误。无论是终止错误还是非终止错误,每个错误都被存储在名为$Error的 PowerShell 自动变量中,按出现的时间顺序排列。

为了演示$Error变量,让我们打开控制台并运行一个你知道会返回非终止错误的命令(清单 5-5)。

PS> Get-Item -Path C:\NotFound.txt
Get-Item : Cannot find path 'C:\NotFound.txt' because it does not exist.
At line:1 char:1
+ Get-Item -Path C:\NotFound.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\NotFound.txt:String) [Get-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand

清单 5-5:示例错误

现在,在相同的 PowerShell 会话中,检查$Error变量(清单 5-6)。

PS> $Error
Get-Item : Cannot find path 'C:\NotFound.txt' because it does not exist.
At line:1 char:1
+ Get-Item -Path C:\NotFound.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\NotFound.txt:String) [Get-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
--snip--

清单 5-6:$Error变量

除非你正在处理一个全新的会话,否则很可能你会看到一长串错误。要访问特定的错误,你可以像访问任何其他数组一样使用索引符号。$Error中的错误会被添加到数组的前面,因此$Error[0]是最新的,$Error[1]是第二新,依此类推。

摘要

PowerShell 中的错误处理是一个庞大的主题,本章仅涵盖了基础知识。如果你想深入了解,可以通过运行Get-Help about_try_catch_finally来查看about_try_catch_finally帮助文档。另一个很好的资源是由 Dave Wyatt 在 DevOps Collective 编写的PowerShell 错误处理大全leanpub.com/thebigbookofpowershellerrorhandling/)。

这里的主要要点是理解终止错误和非终止错误之间的区别,try/catch语句的使用,以及各种ErrorAction选项,这些都将帮助你构建应对代码可能抛出的任何错误所需的技能。

到目前为止,你一直是在一个代码块中完成所有操作。在下一章中,你将看到如何将代码组织成独立的、可执行的单元,称为函数

第六章:编写函数

图片

到目前为止,你编写的代码相对单一:你的脚本只有单一的任务。虽然仅仅是一个能访问文件夹中文件的脚本并没有错,但当你编写更强大的 PowerShell 工具时,你会希望代码能做更多事情。没有什么能阻止你把更多内容塞进一个脚本中。你可以编写一千行代码,做数百个任务,所有这些都写在一个不间断的代码块中。但那个脚本将会是一个混乱,既难以阅读也难以操作。你可以将每个任务拆分成独立的脚本,但那样使用起来也会很混乱。你想要的是一个能够做很多事情的工具,而不是一百个只能做单一任务的工具。

为此,你将把每个任务分解为其自己的函数,一个标记的代码块,执行单一任务。函数只需要定义一次。你只需要编写代码来解决某个问题一次,将其存储在函数中,遇到这个问题时,你只需使用——或调用——解决问题的函数。函数显著提高了代码的可用性和可读性,使得代码更易于操作。在本章中,你将学习如何编写函数,添加和管理函数的参数,并设置函数以接受管道输入。但首先,让我们先了解一些术语。

函数与 cmdlet

如果函数的概念听起来很熟悉,那可能是因为它听起来有点像你在本书中一直使用的 cmdlet,例如Start-ServiceWrite-Host。这些也是被命名的代码块,用来解决单一问题。函数和 cmdlet 之间的区别在于如何创建这些构造。cmdlet 并不是用 PowerShell 编写的。它通常是用另一种语言编写的,通常像 C#,然后编译后在 PowerShell 中提供。另一方面,函数是用 PowerShell 简单的脚本语言编写的。

你可以使用Get-Command cmdlet 和它的CommandType参数查看哪些命令是 cmdlet,哪些是函数,如清单 6-1 所示。

PS> Get-Command –CommandType Function

清单 6-1:显示可用的函数

该命令将显示当前加载到 PowerShell 会话中的所有函数,或显示 PowerShell 可用的模块中的函数(第七章介绍了模块)。要查看其他函数,你必须将它们复制粘贴到控制台中,或将它们添加到可用模块中,或者点源它们(稍后我们也会讨论)。

既然这些问题已经解决,让我们开始编写函数吧。

定义一个函数

在使用函数之前,你需要先定义它。定义函数时,使用function关键字,后跟一个描述性的用户定义名称,再后跟一对大括号。在大括号内是你希望 PowerShell 执行的脚本块。清单 6-2 定义了一个基本的函数,并在控制台中执行它。

PS> function Install-Software { Write-Host 'I installed some software, Yippee!' }
PS> Install-Software
I installed some software, Yippee!

示例 6-2:通过一个简单的函数向控制台输出消息

你定义的函数Install-Software使用Write-Host在控制台显示一条消息。一旦定义,你可以使用这个函数的名称来执行其脚本块中的代码。

函数的名称很重要。你可以给你的函数起任何名字,但这个名字应当描述函数的功能。PowerShell 中的函数命名遵循动词-名词的语法,最佳实践是除非必要,否则始终使用这种语法。你可以使用Get-Verb命令查看推荐的动词列表。名词通常是你所处理的实体的单数形式——在这个例子中是软件。

如果你想改变函数的行为,你可以重新定义它,正如在示例 6-3 中所示。

PS> function Install-Software { Write-Host 'You installed some software, Yay!' }
PS> Install-Software
You installed some software, Yay!

示例 6-3:重新定义 Install-Software 函数以改变其行为

现在你重新定义了Install-Software,它会显示一个稍微不同的消息。

函数可以在脚本中定义,或者直接输入到控制台中。在示例 6-2 中,你定义了一个小函数,所以在控制台中定义它并没有问题。大多数情况下,你会有更大的函数,最好将这些函数定义在脚本或模块中,然后调用这个脚本或模块以将函数加载到内存中。正如你从示例 6-3 中可能想象到的,每次都要重新输入一个百行函数以调整其功能,可能会让人感到有些沮丧。

在本章的其余部分,你将扩展我们的Install-Software函数,使其接受参数并接受管道输入。我建议你在使用你最喜欢的编辑器时,将函数存储为.ps1文件,一边阅读本章内容一边操作。

向函数添加参数

PowerShell 函数可以有任意数量的参数。当你创建自己的函数时,你将有机会添加参数,并决定这些参数如何工作。参数可以是必需的,也可以是可选的,它们可以接受任何值,或者被限制只能接受一个有限列表中的某些参数。

例如,你通过Install-Software函数安装的软件可能有多个版本,但当前,Install-Software函数并没有提供让用户指定想要安装的版本的方式。如果只有你一个人在使用这个函数,你可以每次想要特定版本时重新定义这个函数——但那样会浪费时间,而且容易出错,更不用说你希望其他人也能使用你的代码了。

向函数引入参数使其具有变动性。就像变量让你能够编写处理同一情况多种版本的脚本一样,参数允许你编写一个函数,以多种方式完成同一任务。在本例中,你希望它能够安装同一软件的多个版本,并且在多台计算机上执行此操作。

让我们首先为函数添加一个参数,使你或用户能够指定要安装的版本。

创建一个简单的参数

在函数上创建一个参数需要一个param块,这个块将包含所有函数的参数。你可以通过param关键字后跟圆括号来定义一个param块,如在示例 6-4 中所示。

function Install-Software {
    [CmdletBinding()]
    param()

    Write-Host 'I installed software version 2\. Yippee!' 
}

示例 6-4:定义一个 param 块

到此为止,你的函数的实际功能并没有改变。你只是在安装管道,为函数准备了一个参数。你将使用Write-Host命令来模拟软件安装,以便专注于编写函数。

注意

在这本书的演示中,你将只构建高级函数。也有基础函数,但如今它们通常仅在一些小的、特定的场景中使用。两者的区别非常微妙,不便于详细讨论,但如果你在函数名下看到[CmdletBinding()]引用,或者看到一个参数被定义为[Parameter()],你就知道你在使用的是高级函数。

一旦你添加了param块,就可以通过将参数放入param块的圆括号内来创建参数,正如在示例 6-5 中所示。

function Install-Software {
     [CmdletBinding()]
     param(	
    ❶ [Parameter()]
    ❷ [string] $Version
    )

 ❸ Write-Host "I installed software version $Version. Yippee!" 
}

示例 6-5:创建一个参数

param块内,你首先定义了Parameter块❶。像这里这样的空Parameter块什么都不做,但它是必需的(我将在下一节中解释如何使用它)。

让我们重点关注参数名称前的[string]类型❷。通过将参数类型放在方括号中,放置在参数变量名称之前,你可以将该参数转换为指定类型,这样 PowerShell 将始终尝试将传递给此参数的任何值转换为字符串——如果它还不是字符串的话。在这里,传递给$Version的任何内容都将始终被当作字符串处理。将参数转换为类型不是强制性的,但我强烈推荐这样做,因为明确地定义类型会显著减少未来的错误。

你还将$Version添加到打印语句❸中,这意味着当你运行带有Version参数的Install-Software命令并传递一个版本号时,你应该看到一条说明信息,如在示例 6-6 中所示。

PS> Install-Software -Version 2
I installed software version 2\. Yippee!

示例 6-6:将参数传递给你的函数

你现在已经为函数定义了一个有效的参数。让我们看看你可以如何使用这个参数。

强制参数属性

你可以使用Parameter块来控制各种参数属性,这将允许你改变参数的行为。例如,如果你希望确保任何调用该函数的人都必须传入特定的参数,你可以将该参数定义为Mandatory

默认情况下,参数是可选的。让我们通过在Parameter块中使用Mandatory关键字来强制用户传入版本,如 Listing 6-7 所示。

function Install-Software {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Version
    )

    Write-Host "I installed software version $Version. Yippee!"
}
Install-Software

Listing 6-7:使用强制参数

如果你运行这个,你应该会看到以下提示:

cmdlet Install-Software at command pipeline position 1
Supply values for the following parameters:
Version:

一旦设置了Mandatory属性,在没有传入参数的情况下执行该函数将停止执行,直到用户输入一个值。该函数将等待直到用户为Version参数指定一个值,一旦他们输入了值,PowerShell 将执行该函数并继续执行。为了避免这个提示,只需在调用该函数时使用-ParameterName 语法传递该值——例如,Install-Software -Version 2

默认参数值

你还可以在定义参数时为其指定默认值。当你预计某个参数的大多数时候都会有特定值时,这非常有用。例如,如果你希望在 90%的情况下安装该软件的版本 2,并且不想每次运行该函数时都设置该值,你可以为$Version参数指定默认值2,如 Listing 6-8 所示。

function Install-Software {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Version = 2
    )

    Write-Host "I installed software version $Version. Yippee!"
}
Install-Software

Listing 6-8:使用默认参数值

拥有默认参数并不阻止你传入值。你传入的值将覆盖默认值。

添加参数验证属性

除了让参数变为强制项并赋予默认值外,你还可以通过使用参数验证属性来限制它们的可选值。当可能时,限制用户(甚至你自己!)传递给函数或脚本的信息,将消除函数内部不必要的代码。例如,假设你向Install-Software函数传递了值 3,知道版本 3 是一个存在的版本。你的函数假设每个用户都知道哪些版本是存在的,因此它没有考虑当你尝试指定版本 4 时会发生什么。在这种情况下,函数将无法找到该版本的文件夹,因为它不存在。

在 Listing 6-9 中,你在文件路径中使用了$Version字符串。如果有人传入的值不能完整匹配现有的文件夹名称(例如,SoftwareV3 或 SoftwareV4),代码将会失败。

function Install-Software {
    param(
        [Parameter(Mandatory)]
        [string]$Version
    )
    Get-ChildItem -Path \\SRV1\Installers\SoftwareV$Version
}

Install-Software -Version 3

Listing 6-9:假设参数值

这将导致以下错误:

Get-ChildItem : Cannot find path '\\SRV1\Installers\SoftwareV3' because it does not exist.
At line:7 char:5
+     Get-ChildItem -Path \\SRV1\Installers\SoftwareV3
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (\\SRV1\Installers\SoftwareV3:String)
                              [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

你可以编写错误处理代码来解决这个问题,或者通过要求用户仅传入已存在的软件版本来从根本上解决问题。为了限制用户的输入,你将添加参数验证。

存在多种类型的参数验证,但就你的 Install-Software 函数而言,ValidateSet 属性是最合适的。ValidateSet 属性允许你指定允许用于该参数的值列表。如果你只考虑字符串 1 或 2,你会确保用户只能指定这些值;否则,函数将立即失败并通知用户原因。

让我们在 param 块内添加参数验证属性,紧接在原始 Parameter 块下面,如列表 6-10 所示。

function Install-Software {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('1','2')]
        [string]$Version
    )
    Get-ChildItem -Path \\SRV1\Installers\SoftwareV$Version
}

Install-Software -Version 3

列表 6-10:使用 ValidateSet 参数验证属性

你将项 1 和 2 的集合添加到 ValidateSet 属性的尾部括号内,这告诉 PowerShell,Version 的有效值只能是 1 或 2。如果用户尝试传递集合中没有的值,他们将收到错误信息(请参阅列表 6-11),通知他们只有特定数量的选项可用。

Install-Software : Cannot validate argument on parameter 'Version'. The argument "3" does not
belong to the set "1,2" specified by the ValidateSet attribute.
Supply an argument that is in the set and then try the command again.
At line:1 char:25
+ Install-Software -Version 3
+                         ~~~~
+ CategoryInfo          : InvalidData: (:) [Install-Software],ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Install-Software

列表 6-11:传递一个不在 ValidateSet 块中的参数值

ValidateSet 属性是一个常见的验证属性,但还有其他属性可用。要了解有关参数值如何受限的完整说明,请运行 Get-Help about_Functions_Advanced_Parameters,查看 Functions_Advanced_Parameters 帮助主题。

接受管道输入

到目前为止,你已经创建了一个函数,该函数的参数只能通过典型的 -ParameterName <Value> 语法传递。但是在第三章,你学到了 PowerShell 有一个管道,允许你无缝地将对象从一个命令传递到另一个命令。回想一下,一些函数没有管道功能——在使用自己的函数时,这是你可以控制的。让我们给 Install-Software 函数添加管道功能。

添加另一个参数

首先,你需要向代码中添加另一个参数,用于指定你想要安装软件的计算机。你还需要将该参数添加到 Write-Host 命令中,以模拟安装。列表 6-12 添加了新参数:

function Install-Software {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('1','2')],
        [string]$Version

        [Parameter(Mandatory)]
        [string]$ComputerName
    )
    Write-Host "I installed software version $Version on $ComputerName. Yippee!"

}

Install-Software -Version 2 -ComputerName "SRV1"

列表 6-12:添加 ComputerName 参数

就像 $Version 一样,你已经将 ComputerName 参数添加到 param 块中。

一旦你将 ComputerName 参数添加到函数中,你就可以遍历计算机名称列表,并将计算机名称和版本的值传递给 Install-Software 函数,如下所示:

$computers = @("SRV1", "SRV2", "SRV3")
foreach ($pc in $computers) {
    Install-Software -Version 2 -ComputerName $pc
}

但正如你已经看到的几次,你应该避免使用像这样的 foreach 循环,而是应该使用管道。

使函数支持管道

不幸的是,如果你直接尝试使用管道,将会出现错误。在向函数添加管道支持之前,你应该决定希望函数接受哪种类型的管道输入。正如你在第三章中学到的,PowerShell 函数使用两种类型的管道输入:ByValue(整个对象)和ByPropertyName(单个对象属性)。在这里,由于我们的$computers列表只包含字符串,因此你将通过ByValue传递这些字符串。

要添加管道支持,你需要为你想要支持管道输入的参数添加一个参数属性,使用两个关键字之一:ValueFromPipelineValueFromPipelineByPropertyName,如示例 6-13 所示。

function Install-Software {
    param(
        [Parameter(Mandatory)]
        [string]$Version
        [ValidateSet('1','2')],
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$ComputerName
    )
    Write-Host "I installed software version $Version on $ComputerName. Yippee!"
}

$computers = @("SRV1", "SRV2", "SRV3")
$computers | Install-Software -Version 2

示例 6-13:添加管道支持

再次运行脚本,你应该得到如下结果:

I installed software version 2 on SRV3\. Yippee!

注意,Install-Software仅对数组中的最后一个字符串执行。你将在下一节中看到如何解决这个问题。

添加一个 process 块

要告诉 PowerShell 对每个传入的对象执行此函数,必须包含一个process块。在process块内,放入你希望每次函数接收管道输入时执行的代码。按照示例 6-14 中的方式,向你的脚本添加一个process块。

function Install-Software {
    param(
        [Parameter(Mandatory)]
        [string]$Version
        [ValidateSet('1','2')],
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$ComputerName
    )
 process {
        Write-Host "I installed software version $Version on $ComputerName. Yippee!"
    }
}

$computers = @("SRV1", "SRV2", "SRV3")
$computers | Install-Software -Version 2

示例 6-14:添加一个 process 块

注意,process关键字后跟一对花括号,花括号内包含你的函数要执行的代码。

使用process块后,你应该能看到$computers中所有三台服务器的输出:

I installed software version 2 on SRV1\. Yippee!
I installed software version 2 on SRV2\. Yippee!
I installed software version 2 on SRV3\. Yippee!

process块应该包含你希望执行的主要代码。你还可以使用beginend块来执行在函数调用开始和结束时运行的代码。有关构建高级函数的信息,包括beginprocessend块,请通过运行Get-Help about_Functions_Advanced查看about_Functions_Advanced帮助主题。

总结

函数允许你将代码模块化成独立的构建块。它们不仅帮助你将工作拆分成更小、更易于管理的部分,还迫使你编写可读和可测试的代码。当你为函数使用描述性名称时,代码变得自文档化,任何阅读它的人都能直观地理解它在做什么。

在本章中,你学习了函数的基础知识:如何定义它们,如何指定参数及其属性,以及如何接收管道输入。在下一章中,你将看到如何通过使用模块将多个函数打包在一起。

第七章:探索模块

图片

在前一章节中,你学习了函数。函数将脚本拆分成可管理的单元,使你的代码更加高效、可读。但没有理由认为一个好的函数只能存在于某个脚本或单一会话中。在本章中,你将学习关于模块的内容,它是将一组相似的函数打包在一起,并分发供其他人在多个脚本中使用。

从最基本的形式来看,PowerShell 模块就是一个.psm1文件扩展名的文本文件,并包含一些可选的附加元数据。其他类型的模块,如不符合这个描述的模块,被称为二进制模块动态模块,但它们超出了本书的讨论范围。

任何没有显式放入你会话中的命令,几乎可以肯定都来自一个模块。在本书中,你使用的许多命令都属于微软内置的 PowerShell 模块,但也有第三方模块以及你自己创建的模块。要使用模块,你首先需要安装它。然后,当需要使用模块中的命令时,必须将该模块导入到你的会话中;从 PowerShell v3 开始,PowerShell 会在引用命令时自动导入模块。

本章的开始,你将查看已经安装在你系统中的模块。然后,你将拆解一个模块,了解其不同部分,最后你将学习如何从 PowerShell Gallery 下载并安装 PowerShell 模块。

探索默认模块

PowerShell 默认安装了许多模块。在本节中,你将看到如何从会话中发现并导入模块。

在会话中查找模块

你可以通过使用Get-Module cmdlet(它本身也是一个模块的一部分)来查看导入到当前会话中的模块。Get-Module cmdlet 是一个命令,允许你查看系统上所有可在当前会话中使用的模块。

启动一个全新的 PowerShell 会话并运行Get-Module,如示例 7-1 所示。

PS> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content...
--snip--

示例 7-1:使用Get-Module命令查看导入的模块

你在Get-Module输出中看到的每一行都是已经导入到当前会话中的模块,这意味着该模块中的所有命令都可以立即使用。Microsoft.PowerShell.ManagementMicrosoft.PowerShell.Utility模块是 PowerShell 会话中默认导入的模块。

注意示例 7-1 中的ExportedCommands列。这些是你可以从模块中使用的命令。你可以通过使用Get-Command并指定模块名称,轻松找到所有这些命令。让我们看看示例 7-2 中Microsoft.PowerShell.Management模块中的所有导出命令。

PS> Get-Command -Module Microsoft.PowerShell.Management

CommandType     Name                 Version    Source
-----------     ----                 -------    ------
Cmdlet          Add-Computer         3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Add-Content          3.1.0.0    Microsoft.PowerShell.Management 
--snip--

示例 7-2:查看 PowerShell 模块中的命令

这些是从该模块导出的所有命令;它们是可以从模块外部显式调用的命令。某些模块作者选择在模块中包含用户无法使用的函数。任何未导出给用户,并且仅在脚本或模块内部执行的函数,称为 私有函数,或一些开发者所说的 助手函数

如果不带任何参数使用 Get-Module,它会返回所有已导入的模块,但对于那些已安装但未导入的模块,应该怎么办呢?

在计算机上查找模块

要获取所有已安装且可以导入到会话中的模块列表,你可以使用带有 ListAvailable 参数的 Get-Module,如 清单 7-3 所示。

PS> Get-Module –ListAvailable
   Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name              ExportedCommands
---------- -------    ----              ----------------
Script     1.2        PSReadline        {Get-PSReadlineKeyHandler,Set-PSReadlineKeyHandler...

   Directory:\Modules

ModuleType Version    Name              ExportedCommands
---------- -------    ----              ----------------
Manifest   1.0.0.0    ActiveDirectory   {Add-ADCentralAccessPolicyMember...
Manifest   1.0.0.0    AppBackgroundTask {Disable-AppBackgroundTaskDiagnosticLog...
--snip--

清单 7-3:使用 Get-Module 查看所有可用模块

ListAvailable 参数告诉 PowerShell 检查几个文件夹,查找其中包含 .psm1 文件的子文件夹。然后,PowerShell 会从文件系统读取这些模块,并返回每个模块的名称、一些元数据,以及可以从该模块中使用的所有功能。

PowerShell 会根据模块的类型,在几个默认位置查找磁盘上的模块:

系统模块 几乎所有默认安装的 PowerShell 模块都会位于 C:\Windows\System32\WindowsPowerShell\1.0\Modules。这个模块路径通常仅用于内部 PowerShell 模块。严格来说,你可以将模块放在这个文件夹里,但不建议这样做。

所有用户模块 模块也存储在 C:\Program Files\WindowsPowerShell\Modules。这个路径通常被称为 所有用户 模块路径,这是你可以放置任何希望所有登录计算机的用户都能使用的模块的地方。

当前用户模块 最后,你可以将模块存储在 C:\Users<LoggedInUser>\Documents\WindowsPowerShell\Modules。在这个文件夹中,你会找到所有由你创建或下载的仅对当前用户可用的模块。将模块放在这个路径中,可以实现一些分离,以防多个具有不同需求的用户登录计算机。

当调用 Get-Module -ListAvailable 时,PowerShell 会读取所有这些文件夹路径,并返回每个路径中的所有模块。但是,这些并不是唯一可能的模块路径,只是默认路径。

你可以通过使用 $PSModulePath 环境变量来告诉 PowerShell 添加一个新的模块路径,该变量定义了每个模块文件夹,并用分号分隔,如 清单 7-4 所示。

PS> $env:PSModulePath
C:\Users\Adam\Documents\WindowsPowerShell\Modules;
C:\Program Files\WindowsPowerShell\Modules\Modules;
C:\Program Files (x86)\Microsoft SQL Server\140\Tools\PowerShell\Modules\

清单 7-4:PSModulePath 环境变量

你可以通过对字符串进行解析,向 PSModulePath 环境变量添加文件夹,尽管这种技术可能有点高级。下面是一个简短的命令:

PS> $env:PSModulePath + ';C;\MyNewModulePath'.

然而,要注意,这种更改只会在当前会话中生效。为了使更改持久化,你需要在Environment .NET 类上使用SetEnvironmentVariable()方法,如下所示:

PS> $CurrentValue = [Environment]::GetEnvironmentVariable("PSModulePath", "Machine")
PS> [Environment]::SetEnvironmentVariable("PSModulePath", $CurrentValue + ";C:\
MyNewModulePath", "Machine")

现在让我们看看如何通过导入模块来使用你已有的模块。

导入模块

一旦模块文件夹路径被添加到PSModulePath环境变量中,你就必须将模块导入到当前会话中。如今,由于 PowerShell 的自动导入功能,如果你安装了一个模块,通常可以先调用你想要的函数,PowerShell 会自动导入该函数所属的模块。不过,理解导入机制仍然很重要。

让我们使用一个默认的 PowerShell 模块,叫做Microsoft.PowerShell.Management。在清单 7-5 中,你将运行Get-Module两次:第一次是在一个新的 PowerShell 会话中,第二次是在使用cd命令之后,cdSet-Location的别名,它是Microsoft.PowerShell.Management模块中的一个命令。看看会发生什么:

PS> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type...
Script     1.2        PSReadline                          {Get-PSReadlineKeyHandler... 

PS> cd\
PS> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content...
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type...
Script     1.2        PSReadline                          {Get-PSReadlineKeyHandler....

清单 7-5:使用cd后 PowerShell 自动导入Microsoft.PowerShell.Management

如你所见,Microsoft.PowerShell.Management会在你使用cd后自动导入。自动导入功能通常是有效的。但如果你期望一个模块中的命令可用,而它却不可用,可能是模块本身的问题导致命令未能导入。

要手动导入一个模块,使用Import-Module命令,如清单 7-6 所示。

PS> Import-Module -Name Microsoft.PowerShell.Management
PS> Import-Module -Name Microsoft.PowerShell.Management -Force
PS> Remove-Module -Name Microsoft.PowerShell.Management

清单 7-6:手动导入模块、重新导入模块和移除模块

你会注意到这个清单还使用了Force参数和Remove-Module命令。如果模块已经发生了变化(比如你修改了一个自定义模块),你可以使用带有Force参数的Import-Module命令来卸载并重新导入该模块。Remove-Module会将一个模块从会话中卸载,尽管这个命令并不常用。

PowerShell 模块的组成部分

现在你已经学会了如何使用 PowerShell 模块,让我们看看它们的具体样子。

.psm1 文件

任何带有.psm1文件扩展名的文本文件都可以是 PowerShell 模块。为了让这个文件有用,它必须包含函数。虽然不是严格要求,所有模块中的函数最好围绕相同的概念来构建。例如,清单 7-7 展示了一些与软件安装相关的函数。

function Get-Software {
    param()
}

function Install-Software {
    param()
}

function Remove-Software {
    param()
}

清单 7-7:处理软件安装的函数

请注意,每个命令名称中的名词保持不变,只有动词发生变化。这是构建模块时的最佳实践。如果你发现自己需要更改名词,那么你应该考虑将一个模块拆分为多个模块。

模块清单

除了包含函数的 .psm1 文件外,你还会有一个模块清单,或者一个 .psd1 文件。模块清单 是一个可选但推荐的文本文件,以 PowerShell 哈希表的形式编写。这个哈希表包含描述模块元数据的元素。

虽然可以从头开始创建一个模块清单,但 PowerShell 提供了一个 New-ModuleManifest 命令,可以为你生成一个模板。让我们使用 New-ModuleManifest 为我们的软件包构建一个模块清单,如 清单 7-8 所示。

PS> New-ModuleManifest -Path 'C:\Program Files\WindowsPowerShell\Modules\Software\Software.psd1' 
-Author 'Adam Bertram' -RootModule Software.psm1 
-Description 'This module helps in deploying software.'

清单 7-8:使用 New-ModuleManifest 来构建模块清单

此命令会创建一个 .psd1 文件,内容如下:

#
# Module manifest for module 'Software'
#
# Generated by: Adam Bertram
#
# Generated on: 11/4/2019
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'Software.psm1'

# Version number of this module.
ModuleVersion = '1.0'

# Supported PSEditions
# CompatiblePSEditions = @()

# ID used to uniquely identify this module
GUID = 'c9f51fa4-8a20-4d35-a9e8-1a960566483e'

# Author of this module
Author = 'Adam Bertram'

# Company or vendor of this module
CompanyName = 'Unknown'

# Copyright statement for this module
Copyright = '(c) 2019 Adam Bertram. All rights reserved.'

# Description of the functionality provided by this module
Description = 'This modules helps in deploying software.'

# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
--snip--
}

正如你在运行命令时看到的,我没有为许多字段提供参数。我们不会深入讨论模块清单。现在,只需要知道,至少要定义 RootModuleAuthorDescription,以及可能的 version。所有这些属性都是可选的,但最好养成尽可能多地向模块清单添加信息的习惯。

现在你已经了解了模块的结构,接下来我们来看一下如何下载并安装一个模块。

使用自定义模块

到目前为止,你一直在使用 PowerShell 默认安装的模块。在本节中,你将学习如何查找、安装和卸载自定义模块。

查找模块

模块的最佳部分之一就是共享它们:为什么要浪费时间解决已经解决的问题呢?如果你遇到问题,PowerShell Gallery 里很可能已有解决方案。PowerShell Gallery 是一个包含成千上万个 PowerShell 模块和脚本的存储库,任何有账户的人都可以自由上传或下载。这里有由个人编写的模块,也有由像 Microsoft 这样的大公司编写的模块。

幸运的是,你也可以直接使用 PowerShell 中的 Gallery。PowerShell 有一个内置模块叫做 PowerShellGet,提供了简单易用的命令与 PowerShell Gallery 交互。清单 7-9 使用 Get-Command 来列出 PowerShellGet 命令。

PS> Get-Command -Module PowerShellGet

CommandType     Name                           Version    Source
-----------     ----                           -------    ------
Function        Find-Command                   1.1.3.1    powershellget
Function        Find-DscResource               1.1.3.1    powershellget
Function        Find-Module                    1.1.3.1    powershellget
Function        Find-RoleCapability            1.1.3.1    powershellget
Function        Find-Script                    1.1.3.1    powershellget
Function        Get-InstalledModule            1.1.3.1    powershellget
Function        Get-InstalledScript            1.1.3.1    powershellget
Function        Get-PSRepository               1.1.3.1    powershellget
Function        Install-Module                 1.1.3.1    powershellget
Function        Install-Script                 1.1.3.1    powershellget
Function        New-ScriptFileInfo             1.1.3.1    powershellget
--snip--

清单 7-9:PowerShellGet 命令

PowerShellGet 模块包含用于查找、保存和安装模块的命令,还包括发布你自己的模块。你现在还没有准备好发布模块(你甚至还没创建自己的模块!),所以我们将专注于如何查找和安装来自 PowerShell Gallery 的模块。

要查找一个模块,你可以使用 Find-Module 命令,它允许你在 PowerShell Gallery 中搜索与特定名称匹配的模块。例如,如果你正在寻找用于管理 VMware 基础设施的模块,你可以使用通配符和 Name 参数来查找所有 PowerShell Gallery 中包含 VMware 字样的模块,如 清单 7-10 所示。

PS> Find-Module -Name *VMware*

Version      Name                                Repository      Description
-------      ----                                ----------      -----------
6.5.2.6...   VMware.VimAutomation.Core           PSGallery       This Windows... 
1.0.0.5...   VMware.VimAutomation.Sdk            PSGallery       This Windows...
--snip--

清单 7-10:使用 Find-Module 查找与 VMware 相关的模块

Find-Module 命令不会下载任何内容;它只会显示 PowerShell Gallery 中的内容。在接下来的部分,你将看到如何安装模块。

安装模块

一旦你有了想要安装的模块,可以使用 Install-Module 命令来安装它。Install-Module 命令可以带有 Name 参数,但我们可以使用管道操作,直接将 Find-Module 返回的对象传递给 Install-Module 命令(见清单 7-11)。

请注意,你可能会收到关于不受信任的存储库的警告。你会收到此不受信任的警告,因为默认情况下,Find-Module 命令使用的是一个不受信任的 PowerShell 存储库,这意味着你必须明确告诉 PowerShell 信任该存储库中的所有包。否则,它会提示你运行 Set-PSRepository,如清单 7-11 中所示,以更改该存储库的安装策略。

PS> Find-Module -Name VMware.PowerCLI | Install-Module

Untrusted repository You are installing the modules from an untrusted repository. If you trust
this repository, change its InstallationPolicy value by running the Set-PSRepository cmdlet.
Are you sure you want to install the modules from 'https://www.powershellgallery.com/api/v2/'?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "N"): a
Installing package 'VMware.PowerCLI'
Installing dependent package 'VMware.VimAutomation.Cloud' [oooooooooooooooooooooooooooooooooooo
ooooooooooooooooooooooooo] Installing package 'VMware.VimAutomation.Cloud'
Downloaded 1003175.00 MB out of 1003175.00 MB. [ooooooooooooooooooooooooooooooooooooooooooooooo
oooooooooooooooooooooo]

清单 7-11:使用 Install-Module 命令安装模块

默认情况下,清单 7-11 中的命令将下载模块并将其放置在 C:\Program Files 中的所有用户模块路径下。要检查模块是否在该路径中,你可以使用以下命令:

PS> Get-Module -Name VMware.PowerCLI -ListAvailable | Select-Object –Property ModuleBase

ModuleBase
----------
C:\Program Files\WindowsPowerShell\Modules\VMware.PowerCLI\6.5.3.6870460

卸载模块

刚接触 PowerShell 的新手常常会混淆删除和卸载模块之间的区别。如你在《导入模块》一节中看到的(见第 82 页),你可以使用 Remove-Module移除 PowerShell 会话中的模块。但这只是将模块从会话中卸载,并不会从磁盘上删除该模块。

要从磁盘中删除模块——或卸载它——你必须使用 Uninstall-Module cmdlet。清单 7-12 卸载你刚刚安装的模块。

PS> Uninstall-Module -Name VMware.PowerCLI

清单 7-12:卸载模块

只有从 PowerShell Gallery 下载的模块才能通过 Uninstall-Module 卸载——默认模块是无法被卸载的!

创建你自己的模块

到目前为止,你一直在使用其他人的模块。当然,PowerShell 模块的一个惊人之处在于你可以创建自己的模块并与全世界分享。你将在本书的第三部分中构建一个真实的模块,但现在,让我们来看一下如何将你的软件模块变成一个真正的模块。

如你之前所见,典型的 PowerShell 模块由一个文件夹(模块容器)、一个 .psm1 文件(模块文件)和一个 .psd1 文件(模块清单)组成。如果模块文件夹位于三个位置之一(系统、所有用户或当前用户),PowerShell 将自动识别并导入它。

让我们首先创建模块文件夹。模块文件夹必须与模块本身同名。由于我通常将模块设置为系统中所有用户可用,你将把它添加到所有用户的模块路径中,像这样:

PS> mkdir 'C:\Program Files\WindowsPowerShell\Modules\Software'

一旦创建了文件夹,创建一个空白的 .psm1 文件,该文件最终将保存你的函数:

PS> Add-Content 'C:\Program Files\WindowsPowerShell\Modules\Software\Software.psm1'

接下来,按照你在清单 7-8 中的操作,创建模块清单:

PS> New-ModuleManifest -Path 'C:\Program Files\WindowsPowerShell\Modules\Software\Software.psd1' 
-Author 'Adam Bertram' -RootModule Software.psm1 
-Description 'This module helps in deploying software.'

到此为止,PowerShell 应该能够看到你的模块,但注意它还没有看到任何已导出的命令:

PS> Get-Module -Name Software -List

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                      ExportedCommands
---------- -------    ----                      ----------------
Script     1.0        Software

现在,让我们把你之前使用的三个函数添加到 .psm1 文件中,看看 PowerShell 是否能识别它们:

PS> Get-Module -Name Software -List

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                      ExportedCommands
---------- -------    ----                      ----------------
Script     1.0        Software                  {Get-Software...

PowerShell 已经导出了你模块中的所有命令,并使其可供使用。如果你想更进一步,选择哪些命令被导出,你还可以打开模块清单,找到 FunctionsToExport 键。在那里,你可以定义每个命令,用逗号分隔,这将决定哪些命令被导出。虽然这不是强制性的,但它提供了更细致的模块函数导出控制。

恭喜!你刚刚创建了你的第一个模块!除非你为其中的函数填充实际功能,否则它不会做太多,当然,这也是一个值得你自己完成的有趣挑战。

总结

在本章中,你了解了模块,这是一些志同道合的代码集合,能帮助你避免在已经解决的问题上浪费时间。你看到了模块的基本结构,以及如何安装、导入、移除和卸载它们。你甚至创建了自己的基础模块!

在第八章中,你将学习如何通过使用 PowerShell 远程操作来访问远程计算机。

第八章:远程运行脚本

图片

如果你是一个小型组织中的唯一 IT 人员,可能有多台服务器需要管理。如果你有一个脚本需要运行,你可以登录到每台服务器,打开 PowerShell 控制台并在那里运行你的脚本。但如果你运行一个脚本,执行每台服务器上的特定任务,你可以节省很多时间。在本章中,你将学习如何使用 PowerShell 远程管理来远程运行命令。

PowerShell 远程管理是一个功能,允许用户在一个或多个计算机上的会话中远程执行命令。会话,或更具体地说,PSSession,是 PowerShell 远程管理的术语,指的是在远程计算机上运行 PowerShell 的环境,从中可以执行命令。尽管执行方式不同,但微软的 Sysinternals 工具psexec与此概念相同:你编写在本地计算机上工作的代码,将其发送到远程计算机,并像坐在它前面一样执行该代码。

本章的大部分内容将集中在会话上——它们是什么,如何使用它们,以及完成后如何处理它们——但首先,你需要了解一些关于脚本块的内容。

注意

微软在 PowerShell v2 中引入了 PowerShell 远程管理,它是建立在 Windows 远程管理(WinRM) 服务之上的。因此,偶尔你可能会看到 WinRM 一词,用来指代 PowerShell 远程管理。

使用脚本块

PowerShell 远程管理广泛使用脚本块,它们像函数一样,是封装成一个可执行单元的代码。但它们与函数有几个关键区别:它们是匿名的——或者说没有名称——并且可以赋值给变量。

为了检验这种差异,我们来看一个例子。我们定义一个名为New-Thing的函数,它调用Write-Host在控制台中显示一些文本(见列表 8-1)。

function New-Thing {
    param()
    Write-Host "Hi! I am in New-Thing"
}

New-Thing

列表 8-1:定义New-Thing函数,在控制台窗口显示文本

如果你运行这个脚本,你应该看到它将文本"Hi! I am in New-Thing!"返回到控制台。但请注意,为了得到这个结果,你必须调用New-Thing函数才能执行。

你可以通过首先将脚本块赋值给一个变量来复制New-Thing函数调用的结果,如列表 8-2 所示。

PS> $newThing = { Write-Host "Hi! I am in a scriptblock!" }

列表 8-2:创建脚本块并将其赋值给名为$newThing的变量

要构建一个脚本块,将你想执行的代码放在花括号之间。你将我们的脚本块存储在变量$newThing中,你可能认为要执行这个脚本块,只需调用该变量,如列表 8-3 所示。

PS> $newThing = { Write-Host "Hi! I am in a scriptblock!" }
PS> $newThing
 Write-Host "Hi! I am in a scriptblock!"

列表 8-3:创建并执行脚本块

但正如你所看到的,PowerShell 会字面地读取$newThing的内容。它没有意识到Write-Host是一个应该执行的命令,而是显示了脚本块的值。

要告诉 PowerShell 运行内部的代码,你需要使用&符号(&)后跟变量名。示例 8-4 展示了这种语法。

PS> & $newThing
Hi! I am in a scriptblock!

示例 8-4:执行脚本块

&符号告诉 PowerShell,花括号中的内容是它应该执行的代码。&符号是执行代码块的一种方式;然而,它不允许像命令那样进行定制,而你在使用 PowerShell 远程操作时,通常需要这种定制功能来操作远程计算机。下一节将介绍另一种执行脚本块的方法。

使用Invoke-Command在远程系统上执行代码

在使用 PowerShell 远程操作时,你将使用两个主要命令:Invoke-CommandNew-PSSession。在本节中,你将学习Invoke-Command;下一节将介绍New-PSSession命令。

Invoke-Command可能是你在使用 PowerShell 远程操作时最常用的命令。有两种主要的使用方式。第一种是当你运行我所说的临时命令——你希望执行的小型、一次性的表达式。第二种是使用交互式会话。本章将介绍这两种方式。

一个临时命令的例子是,当你运行Start-Service命令以启动远程计算机上的服务时。当你使用Invoke-Command执行临时命令时,PowerShell 会在后台创建一个会话,并在命令执行完成后立即拆除该会话。这限制了你只能用Invoke-Command做的事情,这就是为什么在下一节你将看到如何创建自己的会话。

但现在,让我们看看Invoke-Command如何与临时命令一起工作。打开你的 PowerShell 控制台,键入Invoke-Command并按 ENTER,正如在示例 8-5 中所示。

PS> Invoke-Command

cmdlet Invoke-Command at command pipeline position 1
Supply values for the following parameters:
ScriptBlock:

示例 8-5:无参数运行Invoke-Command

你的控制台应该立即要求你提供一个脚本块。你将提供hostname命令,它将返回执行命令的计算机的主机名。

要将hostname脚本块传递给Invoke-Command,你需要使用必需的参数ComputerName,它告诉Invoke-Command在哪台远程计算机上运行此命令,正如你在示例 8-6 中看到的那样。(注意,要使其正常工作,我的计算机和远程计算机WEBSRV1必须是同一 Active Directory(AD)域的一部分,并且我的计算机需要在WEBSRV1上拥有管理员权限。)

PS> Invoke-Command -ScriptBlock { hostname } -ComputerName WEBSRV1
WEBSRV1

示例 8-6:运行一个简单的Invoke-Command示例

请注意,hostname的输出现在是远程计算机的名称——在我的系统中,远程计算机名为WEBSRV1。你现在已经成功执行了你的第一个远程命令!

注意

如果您在运行 Windows Server 2012 R2 之前版本的操作系统的远程计算机上尝试此操作,可能无法按预期工作。如果是这种情况,您首先需要启用 PowerShell 远程处理。从 Server 2012 R2 开始,PowerShell 远程处理默认启用,WinRM 服务正在运行,并且所有必要的防火墙端口已打开并设置了访问权限。但如果您运行的是较早版本的 Windows,则必须手动执行此操作,因此在尝试对旧版本的服务器运行 Invoke-Command 之前,首先需要在远程计算机上以提升权限的控制台会话运行 Enable-PSRemoting。您还可以使用 Test-WSMan 命令确认 PowerShell 远程处理是否已配置并可用。

在远程计算机上运行本地脚本

在上一节中,您在远程计算机上执行了脚本块。您还可以使用 Invoke-Command 执行整个脚本。与使用 Scriptblock 参数不同,您可以使用 FilePath 参数并提供本地计算机上的脚本路径。使用 FilePath 参数时,Invoke-Command 会在本地读取脚本内容,然后在远程计算机上执行这些代码。与普遍认知相反,脚本本身并不会在远程计算机上执行。

举个例子,假设您在本地计算机的 *C:* 根目录下有一个名为 GetHostName.ps1 的脚本。该脚本包含一行:hostname。您希望在远程计算机上运行这个脚本以返回计算机的主机名。请注意,虽然我们保持脚本极其简单,但 Invoke-Command 并不关心脚本中的内容。它会高兴地执行其中的任何内容。

要运行脚本,您将脚本文件传递给 Invoke-CommandFilePath 参数,如 清单 8-7 所示。

PS> Invoke-Command -ComputerName WEBSRV1 -FilePath C:\GetHostName.ps1
WEBSRV1

清单 8-7:在远程计算机上运行本地脚本

Invoke-Command 会在 WEBSRV1 计算机上运行 GetHostName.ps1 中的代码,并将输出返回到您的本地会话。

在远程使用本地变量

尽管 PowerShell 远程处理解决了许多问题,但在使用本地变量时仍需小心。假设您在远程计算机上有一个文件路径 C:\File.txt。由于这个文件路径可能会在某个时候发生变化,您可能决定将该路径分配为一个变量,例如 $serverFilePath

PS> $serverFilePath = 'C:\File.txt'

现在,您可能需要在远程脚本块中引用 C:\File.txt 路径。在 清单 8-8 中,您可以看到当您尝试直接引用该变量时会发生什么情况。

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { Write-Host "The value
of foo is $serverFilePath" }
The value of foo is

清单 8-8:本地变量在远程会话中不起作用。

请注意,$serverFilePath 变量没有值,因为在远程计算机上执行的脚本块中,该变量并不存在!当你在脚本或控制台中定义一个变量时,该变量会存储在一个特定的 运行空间 中,这是 PowerShell 用来存储会话信息的容器。如果你尝试同时打开两个 PowerShell 控制台并(未能)在另一个控制台中使用其中的变量,你可能已经遇到过运行空间的概念。

默认情况下,变量、函数和其他构造不能跨多个运行空间传播。然而,你可以使用几种方法在不同的运行空间中使用变量、函数等。有两种主要方法可以将变量传递到远程计算机。

使用 ArgumentList 参数传递变量

要将变量的值传递到远程脚本块中,你可以在 Invoke-Command 上使用 ArgumentList 参数。这个参数允许你将本地值的数组传递到脚本块中,称为 $args,你可以在脚本块的代码中使用它。为了演示这个过程,在 列表 8-9 中,你将传递包含文件路径 C:\File.txt$serverFilePath 变量到远程脚本块,并通过 $args 数组进行引用。

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { Write-Host "The value
of foo is $($args[0])" } -ArgumentList $serverFilePath
The value of foo is C:\File.txt

列表 8-9:使用 $args 数组将本地变量传递到远程会话

正如你所看到的,变量的值 C:\File.txt 现在已在脚本块中。这是因为你将 $serverFilePath 传递到 ArgumentList 中,并且将脚本块内的 $serverFilePath 引用替换为 $args[0]。如果你想传递多个变量到脚本块中,你可以在 ArgumentList 参数值中添加另一个值,并在需要引用新变量的地方将 $args 引用加 1。

使用 $Using 语句传递变量值

将本地变量的值传递给远程脚本块的另一种方法是使用 $using 语句。通过在任何本地变量名前加上 $using,你可以避免使用 ArgumentList 参数。在 PowerShell 将脚本块发送到远程计算机之前,它会查找 $using 语句,并展开脚本块中的所有本地变量。

在 列表 8-10 中,你将重写 列表 8-9,使用 $using:serverFilePath 来代替 ArgumentList

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { Write-Host "The value
of foo is $using:serverFilePath" }
The value of foo is C:\File.txt

列表 8-10:使用 $using 来引用远程会话中的本地变量

正如你所看到的,列表 8-9 和 8-10 的结果是相同的。

$using语句需要的工作更少,也更直观,但将来当你开始编写 Pester 测试脚本时,你会发现可能需要回退到使用ArgumentList参数:当使用$using选项时,Pester 无法评估$using变量中的值。而当使用ArgumentList参数时,传递给远程会话的变量是在本地定义的,Pester 可以解释和理解这些变量。如果现在这不太清楚,等你读到第九章时你就会明白了。现在,$using语句已经非常优秀了!

现在你对Invoke-Command cmdlet 有了基本的了解,让我们学习更多关于会话的内容。

使用会话

如前所述,PowerShell 远程操作使用了一个叫做会话的概念。当你在远程创建会话时,PowerShell 会在远程计算机上打开一个本地会话,你可以使用这个会话在那里执行命令。你不需要了解太多会话的技术细节。你需要知道的是,你可以创建会话、连接到会话、断开会话,并且会话将保持你离开时的状态。会话在你删除它之前不会结束。

在上一节中,当你运行Invoke-Command时,它会启动一个新的会话,运行代码,并且一气呵成地结束会话。在本节中,你将看到如何创建我所称的完整会话,即你可以直接向其中输入命令的会话。使用Invoke-Command执行一次性的临时命令效果很好,但当你需要运行很多不能全部放入单个脚本块的命令时,它就不那么高效了。例如,如果你正在编写一个大型脚本,这个脚本需要本地执行一些工作、从另一个源获取信息、在远程会话中使用这些信息、从远程会话获取信息并返回本地计算机,你就必须创建一个脚本,反复运行Invoke-Command。更麻烦的是,如果你需要在远程会话中设置一个变量并在之后再次使用它,使用目前的Invoke-Command方法是无法实现的——你需要一个在你离开后仍然保持活动的会话。

创建新会话

要在远程计算机上创建一个半永久的会话,进行 PowerShell 远程操作,你必须显式地通过使用New-PSSession命令来创建一个完整会话,这会在远程计算机上创建一个会话,并在本地计算机上创建该会话的引用。

要创建一个新的 PSSession,请使用带有 ComputerName 参数的 New-PSSession,就像在 清单 8-11 中所示。在此示例中,我运行此命令的计算机与 WEBSRV1 处于同一的 Active Directory 域中,并且我以域用户的身份登录 WEBSRV1,具有管理员权限。要通过使用 ComputerName 参数进行连接(就像我在 清单 8-11 中所做的那样),用户必须是本地管理员或者至少是远程管理用户组中的成员。如果您不在 AD 域中,可以在 New-PSSession 上使用 Credential 参数,传递一个包含用于身份验证到远程计算机的备用凭据的 PSCredential 对象。

PS> New-PSSession -ComputerName WEBSRV1

 Id Name        ComputerName   ComputerType    State    ConfigurationName      Availability
 -- ----        ------------   ------------    -----    -----------------      ------------
  3 WinRM3      WEBSRV1        RemoteMachine   Opened   Microsoft.PowerShell   Available

清单 8-11:创建一个新的 PSSession

如您所见,New-PSSession 返回一个会话。一旦建立了会话,您可以通过 Invoke-Command 跳入和跳出会话;与使用临时命令时不同,您将必须使用 Session 参数。

您需要使用 Session 参数并提供一个会话对象。您可以使用 Get-PSSession 命令查看所有当前会话。在 清单 8-12 中,您将会将 Get-PSSession 的输出存储在一个变量中。

PS> $session = Get-PSSession
PS> $session

 Id    Name     ComputerName   ComputerType    State    ConfigurationName      Availability
 --    ----     ------------   ------------    -----    -----------------      ------------
  6    WinRM6   WEBSRV1        RemoteMachine   Opened   Microsoft.PowerShell   Available

清单 8-12:查找在本地计算机上创建的会话

因为您只运行了一次 New-PSSession,所以在 清单 8-12 中只创建了一个 PSSession。如果您有多个会话,可以通过使用 Get-PSSession 命令的 Id 参数来选择 Invoke-Command 要使用的会话。

在会话中调用命令

现在您有了一个存储在变量中的会话,可以将该变量传递给 Invoke-Command 并在会话中运行一些代码,就像在 清单 8-13 中所示。

PS> Invoke-Command -Session $session -ScriptBlock { hostname }
WEBSRV1

清单 8-13:使用现有会话在远程计算机上调用命令

您应该注意到,此命令的运行速度比您传递命令时要快得多。这是因为 Invoke-Command 不需要创建和拆除一个新的会话。当您创建完整会话时,不仅速度更快,而且还可以访问更多功能。例如,正如您在 清单 8-14 中所见,您可以在远程会话中设置变量,并且返回到会话时不会丢失这些变量。

PS> Invoke-Command -Session $session -ScriptBlock { $foo = 'Please be here next time' }
PS> Invoke-Command -Session $session -ScriptBlock { $foo }
Please be here next time

清单 8-14:变量值在后续会话连接中保持不变。

只要会话保持打开状态,您可以在远程会话中执行任何您需要的操作,会话的状态将保持不变。但是,这仅适用于当前的本地会话。如果启动另一个 PowerShell 进程,您不能继续之前的操作。远程会话仍然处于活动状态,但是本地计算机对该远程会话的引用将会丢失。在这种情况下,PSSession 将进入断开连接状态(您将在后面的部分看到)。

打开交互式会话

Listing 8-14 使用 Invoke-Command 向远程计算机发送命令并接收响应。像这样运行远程命令就像运行一个没有监控的脚本。它不是交互式的,就像你在 PowerShell 控制台中输入按键一样。如果你想为远程计算机上运行的会话打开一个交互式控制台——例如进行故障排除——你可以使用 Enter-PSSession 命令。

Enter-PSSession 命令允许用户以交互方式操作会话。它可以创建自己的会话,也可以依赖于通过 New-PSSession 创建的现有会话。如果没有指定要进入的会话,Enter-PSSession 将创建一个新的会话并等待进一步输入,如 Listing 8-15 所示。

PS> Enter-PSSession -ComputerName WEBSRV1
[WEBSRV1]: PS C:\Users\Adam\Documents>

Listing 8-15: 进入交互式会话

请注意,你的 PowerShell 提示符已经变为 [WEBSRV1]: PS。这个提示符表示你不再在本地运行命令,而是在那个远程会话中。此时,你可以像在远程计算机的控制台上一样运行任何命令。像这样交互式地操作会话是避免使用 远程桌面协议RDP)应用程序启动交互式 GUI 来执行任务(例如远程计算机的故障排除)的好方法。

断开和重新连接会话

如果你关闭了 PowerShell 控制台,然后重新打开它,再尝试在之前工作的会话中使用 Invoke-Command,你将收到一条错误消息,如 Listing 8-16 所示。

PS> $session = Get-PSSession -ComputerName websrv1
PS> Invoke-Command -Session $session -ScriptBlock { $foo }
Invoke-Command : Because the session state for session WinRM6, a617c702-ed92
-4de6-8800-40bbd4e1b20c, websrv1 is not equal to Open, you cannot run a
command in the session. The session state is Disconnected.
At line:1 char:1
+ Invoke-Command -Session $session -ScriptBlock { $foo }
--snip--

Listing 8-16: 尝试在断开的会话中运行命令

PowerShell 能够在远程计算机上找到 PSSession,但在本地计算机上找不到该引用,这表明会话已断开。如果没有正确断开本地会话对远程 PSSession 的引用,就会发生这种情况。

你可以使用 Disconnect-PSSession 命令断开现有会话。你可以通过 Get-PSSession 检索之前创建的会话,然后将这些会话传递给 Disconnect-PSSession 命令进行清理(参见 Listing 8-17)。或者,您还可以使用 Disconnect-PSSessionSession 参数一次断开一个会话。

PS> Get-PSSession | Disconnect-PSSession

Id Name          ComputerName   ComputerType    State          ConfigurationName    Availability
-- ----          ------------   ------------    -----          -----------------    ------------
 4 WinRM4        WEBSRV1        RemoteMachine   Disconnected   Microsoft.PowerShell None

Listing 8-17: 断开 PSSession 连接

要正确地断开会话,你可以通过显式调用 Disconnect-PSSession -Session 会话名称,或通过 Get-PSSession 将现有会话传递给命令来将远程会话名称传递给 Session 参数,如 Listing 8-17 所示。

如果你想在稍后重新连接会话,在使用Disconnect-PSSession断开连接后,关闭你的 PowerShell 控制台,然后使用Connect-PSSession命令,如示例 8-18 所示。请注意,你只能看到并连接到已断开的会话,这些会话必须是你账户之前创建的。你将无法看到其他用户创建的会话。

PS> Connect-PSSession -ComputerName websrv1
[WEBSRV1]: PS>

示例 8-18:重新连接到PSSession

现在你应该能够像从未关闭过控制台一样,在远程计算机上运行代码。

如果你仍然收到错误信息,可能是 PowerShell 版本不匹配。断开会话仅在本地计算机和远程服务器的 PowerShell 版本相同的情况下有效。例如,如果本地计算机上运行 PowerShell 5.1,而你连接的远程服务器正在运行一个不支持断开会话的版本(例如 PowerShell v2 或更早版本),则断开会话将无法正常工作。始终确保本地计算机和远程服务器使用相同版本的 PowerShell。

要检查本地计算机的 PowerShell 版本是否与远程计算机的版本匹配,请检查$PSVersionTable变量的值,该变量包含版本信息(见示例 8-19)。

PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.15063.674
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.15063.674
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

示例 8-19:检查本地计算机上的 PowerShell 版本

要检查远程计算机上的版本,可以在该计算机上运行Invoke-Command,并传递$PSVersionTable变量,如示例 8-20 所示。

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { $PSVersionTable }

Name                           Value
----                           -----
PSRemotingProtocolVersion      2.2
BuildVersion                   6.3.9600.16394
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0}
PSVersion                      4.0
CLRVersion                     4.0.30319.34014
WSManStackVersion              3.0
SerializationVersion           1.1.0.1

示例 8-20:检查远程计算机上的 PowerShell 版本

我建议,在断开连接之前,你检查一下版本是否匹配;这样,你就能避免在远程系统上丢失宝贵的工作。

使用 Remove-PSSession 删除会话

每当New-PSSession命令创建一个新会话时,该会话会同时存在于远程服务器和本地计算机上。你也可以同时在多个服务器上打开多个会话,如果其中一些会话不再使用,你可能最终需要清理它们。你可以使用Remove-PSSession命令来执行此操作,该命令会访问远程计算机,关闭该会话,并在存在的情况下,移除本地的PSSession引用。示例 8-21 就是一个例子:

PS> Get-PSSession | Remove-PSSession
PS> Get-PSSession

示例 8-21:删除PSSession

这里,你再次运行Get-PSSession,但没有返回任何结果。这意味着本地计算机上没有会话。

理解 PowerShell 远程身份验证

到目前为止,我一直没有涉及身份验证的问题。默认情况下,如果本地计算机和远程计算机都在同一个域中,并且都启用了 PowerShell 远程功能,你无需显式身份验证。但是,如果它们不在同一域中,你需要以某种方式进行身份验证。

使用 PowerShell 远程连接到远程计算机的两种最常见身份验证方式是通过 Kerberos 或 CredSSP。如果你处于一个 Active Directory 域中,你很可能已经在使用 Kerberos 票证系统,无论你是否意识到这一点。Active Directory 和一些 Linux 系统使用 Kerberos 领域,它们向客户端颁发票证。这些票证随后会被提交给资源并与域控制器(在 Active Directory 中)进行比对。

另一方面,CredSSP 不需要 Active Directory。CredSSP 从 Windows Vista 开始就已被引入,它使用客户端凭据服务提供程序(CSP)来使应用程序能够将用户凭据委托给远程计算机。CredSSP 不需要像域控制器这样的外部系统来进行两台系统的身份验证。

在 Active Directory 环境中,PowerShell 远程使用 Kerberos 网络身份验证协议向 Active Directory 发起调用,所有身份验证操作都在后台完成。PowerShell 使用你本地登录的账户作为远程计算机的用户身份进行身份验证——就像许多其他服务一样。这就是单点登录的优势所在。

但有时如果你不在 Active Directory 环境中,你就不得不稍微改变身份验证类型;例如,当你需要通过远程计算机上的本地凭据连接到远程计算机,无论是通过互联网还是本地网络时。PowerShell 支持多种 PowerShell 远程身份验证方法,但最常见的——除了使用 Kerberos 之外——是 CredSSP,它允许本地计算机将用户凭据委托给远程计算机。这个概念类似于 Kerberos,但不需要 Active Directory。

在 Active Directory 环境中工作时,通常不需要使用不同的身份验证类型,但有时会遇到这种情况,因此最好做好准备。在本节中,你将学习一个常见的身份验证问题以及如何绕过它。

双跳问题

双跳问题 自从 Microsoft 添加了 PowerShell 远程功能以来一直存在。当你在远程会话中运行代码,然后尝试从该远程会话访问远程资源时,就会出现这个问题。例如,如果你的网络中有一个名为 DC 的域控制器,并且你想通过 C$ 管理共享查看 *C:* 根目录下的文件,你可以从本地计算机远程浏览该共享,毫无问题(参见 示例 8-22)。

PS> Get-ChildItem -Path '\\dc\c$'

    Directory: \\dc\c$

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        10/1/2019  12:05 PM                FileShare
d-----       11/24/2019   2:28 PM                inetpub
d-----       11/22/2019   6:37 PM                InstallWindowsFeature
d-----        4/16/2019   1:10 PM                Iperf

示例 8-22:列举 UNC 共享上的文件

这个问题出现在你创建了一个 PSSession 并尝试重新运行相同的命令时,如 示例 8-23 中所示。

PS> Enter-PSSession -ComputerName WEBSRV1
[WEBSRV1]: PS> Get-ChildItem -Path '\\dc\c$'
ls : Access is denied
--snip--
[WEBSRV1]: PS>

示例 8-23:尝试在会话中访问网络资源

在这种情况下,即使你知道你的用户账户有访问权限,PowerShell 仍然告诉你访问被拒绝。这是因为,当你使用默认的 Kerberos 身份验证时,PowerShell 远程操作并不会将凭据传递给其他网络资源。换句话说,它没有完成两个跳跃。出于安全原因,PowerShell 遵循 Windows 限制,拒绝委派这些凭据,结果返回“访问被拒绝”的消息。

使用 CredSSP 的双跳问题

在本节中,你将学习如何解决双跳问题。我使用“解决”而不是“修复”有原因。微软已警告,使用 CredSSP 是一个安全问题,因为传递给第一个计算机的凭据会自动用于从该计算机进行的所有连接。这意味着,如果原始计算机被攻破,那么可以利用该凭据从该计算机连接到网络上的其他计算机。尽管如此,除了使用一些复杂的变通方法,如基于资源的 Kerberos 受限委派,许多用户还是选择使用 CredSSP 方法,因为它容易使用。

在实现 CredSSP 之前,你必须在客户端和服务器上都启用它,可以通过在提升的 PowerShell 会话中使用 Enable-WsManCredSSP 命令来实现。此命令有一个 Role 参数,允许你定义是启用客户端还是服务器端的 CredSSP。首先,在客户端启用 CredSSP,如 清单 8-24 所示。

注意

要使 CredSSP 生效,可能需要放宽本地策略。如果在尝试启用 CredSSP 时收到权限错误,请确保通过运行 gpedit.msc 并在计算机配置 ▶ 管理模板 ▶ 系统 ▶ 凭据委派下启用“允许仅使用 NTLM 服务器身份验证的保存凭据委派”设置。在该策略中,点击 显示 按钮并输入 WSMAN/ 以允许从任何端点委派凭据。*

PS> Enable-WSManCredSSP ❶-Role ❷Client ❸-DelegateComputer WEBSRV1 -Force

CredSSP Authentication Configuration for WS-Management
CredSSP authentication allows the user credentials on this computer to be sent
to a remote computer. If you use CredSSP authentication for a connection to
a malicious or compromised computer, that machine will have access to your
username and password. For more information, see the Enable-WSManCredSSP Help
topic.
Do you want to enable CredSSP authentication?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): y

cfg         : http://schemas.microsoft.com/wbem/wsman/1/config/client/auth
lang        : en-US
Basic       : true
Digest      : true
Kerberos    : true
Negotiate   : true
Certificate : true
CredSSP     : true

清单 8-24:在客户端计算机上启用 CredSSP 支持

通过将值 Client ❷ 传递给 Role 参数 ❶,你可以在客户端启用 CredSSP。你还需要使用必需的 DelegateComputer 参数 ❸,因为 PowerShell 需要知道哪些计算机被允许使用你将委派的凭据。你可以将星号(*)传递给 DelegateComputer,以允许将凭据委派给所有计算机,但出于安全原因,最好只允许你正在使用的计算机,在这种情况下是 WEBSRV1

一旦在客户端启用了 CredSSP,你需要在服务器上执行相同的操作(清单 8-25)。幸运的是,你可以直接打开一个新的远程会话而不使用 CredSSP,然后在会话内启用 CredSSP,而不必使用 Microsoft 远程桌面访问服务器或亲自访问它。

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { Enable-WSManCredSSP -Role Server }

CredSSP Authentication Configuration for WS-Management CredSSP authentication allows the server
to accept user credentials from a remote computer. If you enable CredSSP authentication on the
server, the server will have access to the username and password of the
client computer if the client computer sends them. For more information, see the Enable-WSManCredSSP Help topic.
Do you want to enable CredSSP authentication?
[Y] Yes  [N] No  [?] Help (default is "Y"): y

#text
-----
False
True
True
False
True
Relaxed

清单 8-25:在服务器计算机上启用 CredSSP 支持

这样,你就已在客户端和服务器上启用了 CredSSP:客户端允许将用户凭据委托给远程服务器,而远程服务器本身也启用了 CredSSP。现在,你可以再次尝试从该远程会话访问远程网络资源(参见清单 8-26)。请注意,如果你需要撤销启用 CredSSP,命令Disable-WsmanCredSSP将恢复你的更改。

PS> Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { Get-ChildItem -Path '\\dc\c$'  } 
❶-Authentication Credssp ❷-Credential (Get-Credential)

cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
Credential

    Directory: \\dc\c$

Mode                LastWriteTime         Length Name                            PSComputerName
----                -------------         ------ ----                            --------------
d-----        10/1/2019  12:05 PM                FileShare                       WEBSRV1
d-----       11/24/2019   2:28 PM                inetpub                         WEBSRV1
d-----       11/22/2019   6:37 PM                InstallWindowsFeature           WEBSRV1
d-----        4/16/2019   1:10 PM                Iperf                           WEBSRV1

清单 8-26:通过 CredSSP 认证的会话访问网络资源

请注意,你必须明确告诉Invoke-Command(或Enter-PSSession)你希望使用 CredSSP 认证❶,并且无论你使用哪个命令,都需要提供凭据。你可以通过使用Get-Credential命令来获取凭据,而不是默认的 Kerberos 认证❷。

执行Invoke-Command并为Get-Credential提供具有访问 DC 上c$共享的用户名和密码后,你可以看到Get-ChildItem命令按预期工作!

总结

PowerShell 远程执行是目前最简单的在远程系统上执行代码的方法。如你在本章所学,PowerShell 远程执行功能易于使用且直观。一旦你掌握了脚本块的概念以及其中代码的执行位置,远程脚本块将成为你的第二天性。

在本书的第三部分—你将构建自己的强大 PowerShell 模块—你几乎会在每个命令中使用 PowerShell 远程执行。如果你在本章中遇到困难,请再读一遍或开始尝试实验。尝试不同的场景,打破它们,修复它们,做任何你能做的事情来理解 PowerShell 远程执行。这是你从本书中可以学习的最重要的技能之一。

第九章介绍了另一个重要技能:使用 Pester 进行测试。

第九章:使用 Pester 进行测试

图片

无法回避:你需要测试你的代码。很容易认为你的代码没有缺陷;但更容易的事是,证明你错了。当你使用 Pester 进行测试时,你可以停止假设,开始了解。

测试已经成为传统软件开发的一个特点,已经有几十年历史了。但是,尽管像单元测试功能测试集成测试验收测试这样的概念对于经验丰富的软件开发人员来说很熟悉,但对于脚本编写者——那些想用 PowerShell 自动化但不拥有软件工程师职位的人来说,这些概念相对较新。由于许多组织越来越依赖 PowerShell 代码来运行关键的生产系统,我们将借鉴编程世界的经验,并将其应用到 PowerShell 中。

在本章中,你将学习如何为你的脚本和模块创建测试,这样你就可以确保代码正常工作,并在你更改代码时保持其稳定性。你将通过被称为 Pester 的测试框架来实现这一点。

引入 Pester

Pester是一个开源的 PowerShell 测试模块,能够在 PowerShell Gallery 中获取。由于其高效且用 PowerShell 编写,它已经成为 PowerShell 测试的事实标准。它允许你编写多种类型的测试,包括单元测试、集成测试和验收测试。如果这些测试名称让你感到陌生,别担心。在本书中,我们将仅使用 Pester 测试一些环境变化,比如虚拟机是否使用正确的名称创建、IIS 是否安装、操作系统是否正确安装。我们将这些测试称为基础设施测试

我们不会涵盖如何测试诸如函数是否被调用、变量是否被正确设置或脚本是否返回特定对象类型之类的内容——这些都属于单元测试的范畴。如果你对 Pester 中的单元测试感兴趣,并想学习如何在不同情况下使用 Pester,可以查阅Pester 书籍(LeanPub,2019 年,leanpub.com/pesterbook/),该书几乎解释了你需要了解的所有关于 PowerShell 测试的内容。

Pester 基础知识

要使用 Pester,你必须先安装它。如果你使用的是 Windows 10,Pester 默认已经安装,但如果你使用的是其他 Windows 操作系统,它也可以通过 PowerShell Gallery 获取。如果你使用的是 Windows 10,Pester 很可能已经过时了,因此你最好从 PowerShell Gallery 获取最新版本。由于 Pester 通过 PowerShell Gallery 提供,你可以运行Install-Module -Name Pester来下载并安装它。安装后,它将包含你需要的所有命令。

值得一提的是,你将使用 Pester 来编写和运行基础设施测试,目的是验证脚本对环境所做的任何预期更改。例如,在通过 Test-Path 创建一个新文件路径后,你可能会运行一个基础设施测试来确保文件路径已被创建。基础设施测试是一种保障措施,用来确认你的代码按预期执行了任务。

一个 Pester 文件

在最基本的形式中,Pester 测试脚本由一个以 .Tests.ps1 结尾的 PowerShell 脚本组成。你可以随意命名主脚本;命名约定和测试结构完全由你决定。在这里,你将脚本命名为 Sample.Tests.ps1

Pester 测试脚本的基本结构是一个或多个 describe 块,每个 describe 块包含(可选的)context 块,每个 context 块又包含 it 块,而每个 it 块包含断言。如果这听起来有点复杂, 列表 9-1 提供了一个视觉指南。

C:\Sample.Tests.ps1
    describe
        context
          it
            assertions

列表 9-1:基础 Pester 测试结构

让我们逐一了解这些部分。

describe

describe 块是一种将相似测试分组在一起的方法。在 列表 9-2 中,你创建了一个名为 IISdescribe 块,它可以用于包括所有测试 Windows 功能、应用池和网站的代码。

describe 块的基本语法是单词 describe 后跟一个名称(用单引号括起来),然后是一个开括号和闭括号。

describe 'IIS' {
}

列表 9-2:Pester describe

尽管这个结构看起来像是一个 if/then 条件,但不要被误导!这是一个传递给 describe 函数的脚本块。请注意,如果你是那种喜欢将大括号放在新行上的人,你可能会失望:开大括号必须与 describe 关键字在同一行。

context

一旦创建了 describe 块,你可以添加一个可选的 context 块。context 块将类似的 it 块组合在一起,这有助于在进行基础设施测试时组织测试。在 列表 9-3 中,你将添加一个 context 块,它将包含所有与 Windows 功能相关的测试。将测试按这种方式分类在 context 块中是个好主意,可以更轻松地管理它们。

describe 'IIS' {
    context 'Windows features' {
    }
}

列表 9-3:Pester context

虽然 context 块是可选的,但当你创建了数十个或数百个组件的测试时,它将变得极为重要!

it

现在让我们在 context 块中添加一个 it 块。it 块是一个更小的组件,用于标记实际的测试。其语法如 列表 9-4 所示,包含一个名称后跟一个块,就像你在 describe 块中看到的那样。

describe 'IIS' {
    context 'Windows features' {
        it 'installs the Web-Server Windows feature' {
        }
    }
}

列表 9-4:带有 contextit 块的 Pester describe

请注意,到目前为止,你更多的是为测试添加了不同的标签,并且这些标签的作用范围有所不同。在接下来的部分,你将添加实际的测试内容。

断言

it 块内,你包含了一个或多个断言。断言 可以看作是实际的测试,或者是比较预期状态和实际状态的代码。Pester 中最常见的断言是 should 断言。should 断言有不同的运算符可以与之配合使用,如 bebeinbelessthan 等。如果你想查看完整的运算符列表,Pester 的 Wiki (github.com/pester/Pester/wiki/) 提供了完整的列表。

在我们的 IIS 示例中,我们来检查名为 test 的应用池是否已在我们的服务器上创建。为此,你首先需要编写代码来查找服务器上 Web-Server Windows 特性的当前状态(我们将其称为 WEBSRV1)。经过一些调查,浏览可用的 PowerShell 命令并筛选 Get-WindowsFeature 命令的帮助文档后,你发现完成此操作的代码如下:

PS> (Get-WindowsFeature -ComputerName WEBSRV1 -Name Web-Server).Installed
True

你知道,如果安装了 Web-Server 特性,Installed 属性将返回 True;否则,它将返回 False。知道这一点后,你可以断言,当你运行这个 Get-WindowsFeature 命令时,你期望 Installed 属性为 True。你想测试这个命令的输出是否等于 True。你可以在 it 块中表示这种情况,如示例 9-5 所示。

describe 'IIS' {
    context 'Windows features' {
        it 'installs the Web-Server Windows feature' {
            $parameters = @{
 ComputerName = 'WEBSRV1'
                  Name         = 'Web-Server'
            }
            (Get-WindowsFeature @parameters).Installed | should -Be $true
        }
    }
}

示例 9-5:使用 Pester 断言测试条件

在这里,你创建了一个基础的 Pester 测试,用来测试 Windows 特性是否已安装。你首先输入你想要运行的测试,然后通过管道将测试结果传递给你的测试条件,在本例中是 should be $true

编写 Pester 测试还有更多内容,我鼓励你通过 《Pester 手册》 (leanpub.com/pesterbook/) 或者通过 4sysops 上的一系列文章 (4sysops.com/archives/powershell-pester-testing-getting-started/)来学习更多细节。这些内容应该足够让你理解我在本书中提供的测试。一旦你完成这本书,编写自己的 Pester 测试将是测试你 PowerShell 技能的一个好方法。

现在你有了一个 Pester 脚本。当然,一旦你有了脚本,你需要运行它!

执行 Pester 测试

使用 Pester 执行测试的最常见方法是使用 Invoke-Pester 命令。这个命令是 Pester 模块的一部分,允许测试者传递测试脚本的路径,Pester 然后会解释并执行该脚本,如示例 9-6 所示。

PS> Invoke-Pester -Path C:\Sample.Tests.ps1
Executing all tests in 'C:\Sample.Tests.ps1'

Executing script C:\Sample.Tests.ps1

  Describing IIS
    [+] installs the Web-Server Windows feature 2.85s
Tests completed in 2.85s
Tests Passed: 1, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0

示例 9-6:运行 Pester 测试

你可以看到,Invoke-Pester命令已执行了Sample.Tests.ps1脚本,并提供了基本信息,如显示describe块的名称、测试结果,以及在该测试运行期间执行的所有测试的摘要。请注意,Invoke-Pester命令将始终显示每个执行的测试状态摘要。在本例中,installs the Web-Server Windows feature测试成功,通过+符号和绿色输出进行指示。

总结

本章介绍了 Pester 测试框架的基础知识。你已经下载、安装并构建了一个简单的 Pester 测试。这应该帮助你理解 Pester 测试的结构以及如何执行它。在接下来的章节中,你将反复使用这个框架。你将添加大量的describe块、it块和各种断言,但基本结构将保持相对不变。

这标志着我们第一部分的最后一章结束。你已经了解了在使用 PowerShell 编写脚本时需要用到的基本语法和概念。现在,让我们进入第二部分,开始动手实践,解决实际问题!

第二部分

自动化日常任务

如果第一部分感觉更像是一个学校练习,而不是实际的内容——那种你可以今天、现在就能做的事情——别担心,你并不孤单!我也有同样的感觉。但你不能总是直接跳进去;有时候在跳入之前先试探一下水温是件好事。这就是第一部分的作用——为那些新接触 PowerShell 的人提供了入门,为那些不陌生的人提供了复习。

在第二部分,你将最终接触到有趣的内容,将你在第一部分学到的知识应用到实际场景中。你将学习如何使用 PowerShell 来自动化一些许多技术专业人员每天都会遇到的常见场景。如果你是一个经验丰富的技术专业人士,你无疑以前遇到过这些场景:昏昏欲睡地在 Active Directory 中点击,花费过多时间在 Excel 表格之间复制粘贴,在你尝试设置某些远程控制软件以便同时连接一打机器时慌乱不已,以便将你上司两天前想要的管理信息发送给他。

在本书的这一部分,你将学习自动化这些任务所需的工具。当然,我无法涵盖所有内容——有太多无聊的任务需要自动化了!接下来是我在 20 年的行业经验中遇到的一些常见场景。如果你遇到的问题没有被覆盖,不用担心!到本书结束时,你将拥有自己解决任务自动化所需的基础知识。

第二部分被分为四个主要主题,涵盖五个章节。

处理结构化数据

数据无处不在。如果你以前处理过数据,你就知道它有成千上万种格式:SQL 数据库、XML 文件、JSON 对象、CSV 文件等等。每种类型的数据都有其特定的结构,而每种结构都必须以不同的方式处理。在第十章,你将学习如何读取、写入和修改各种形式的数据。

自动化 Active Directory 任务

Active Directory(AD)是一个目录服务。从高层次来看,你可以将目录服务看作一种层次化的方式,用来跟踪用户可以访问哪些 IT 资源。AD 是微软版本的目录服务,正如你所想,它被全球成千上万的组织使用,这使得它成为一个非常适合自动化的领域。

第十一章介绍了如何从 PowerShell 控制台管理各种 AD 对象的基本知识。一旦你熟悉了 AD cmdlet,我们将通过一些小项目,帮助你使用各种 AD cmdlet 来自动化你可能会看到的一些最琐碎的任务。

控制云端

像如今几乎所有的技术一样,云计算也得到了 PowerShell 的强力支持。理解 PowerShell 在云环境中的工作方式,包括 Microsoft Azure 和 Amazon Web Services(AWS),将开启自动化的新天地。在第十二章和第十三章中,你将创建虚拟机、Web 服务等。你甚至会看到一个使用 PowerShell 同时与两个云服务提供商互动的示例。由于 PowerShell 对使用哪种云计算环境不偏不倚(它是云中立的),我们可以控制任何我们想要的云!

创建服务器清单脚本

因为本书内容是逐步积累的,你需要在尝试第三部分的技术奇才之前,打好坚实的基础。这正是第十四章的重点,将你在本书中收集的所有知识融合在一起,转化为一个完整的项目。在这里,你将学习如何将来自不同来源的信息整合成一个统一且连贯的报告。这将包括从 AD 查询计算机,并利用 CIM/WMI 对其进行询问,以访问有用的信息,例如名称、RAM、CPU 速度、操作系统、IP 地址等。

总结

到本部分结束时,你应该对可以用 PowerShell 自动化的各种日常任务有一个清晰的了解。通过亲自自动化其中几个任务,你会发现根本无需为昂贵的软件或花哨的顾问支付高额费用来管理你的环境。PowerShell 能够与数百种产品和服务互补——只要有意愿(和 PowerShell),就一定有办法。

第十章:解析结构化数据

Images

PowerShell 内置支持任何 .NET 对象,以及几乎所有你能想到的 shell 方法,因此它能够读取、更新和删除来自多个数据源的数据。如果你的数据以某种结构化的方式存储,处理这些数据会变得更加简单。

在本章中,我们将关注几种常见的结构化数据形式,包括 CSV、Microsoft Excel 电子表格和 JSON。你将学习如何使用 PowerShell 的本机 cmdlet 和 .NET 对象来管理每种数据类型。到本章结束时,你应该成为一个数据处理高手,能够使用 PowerShell 管理各种结构化数据。

CSV 文件

存储数据的最简单、最常见方式之一就是使用 CSV 文件。CSV 文件是一个简单的文本文件,表示一个表格。表格中的每一项由一个共享的、预定的符号(称为分隔符)分隔开来(逗号是最常见的分隔符)。每个 CSV 文件都具有相同的基本结构:CSV 文件的第一行是标题行,包含表格列的所有标题;随后的行包含表格的所有内容。

在这一节中,你将主要使用两个 CSV cmdlet:Import-CsvExport-Csv

读取 CSV 文件

在 PowerShell 能够执行的所有 CSV 处理任务中,最常见的任务几乎肯定是读取。鉴于 CSV 结构的简单性和有效性,CSV 文件在科技界的公司和应用程序中被广泛使用,因此 Import-Csv PowerShell 命令也非常流行。

那么,究竟什么是读取 CSV 文件呢?虽然 CSV 文件包含了你需要的所有信息,但你不能直接将其导入到程序中;通常,你需要读取文件并将其转换为可用的数据。这个过程被称为解析Import-Csv 命令解析 CSV 文件:读取它,并将数据转换为 PowerShell 对象。我稍后会详细介绍 Import-Csv 的使用方法,但首先,值得深入了解 Import-Csv 的底层操作。

我们从一个包含几位员工的简单电子表格开始,数据来自一个虚构公司的图 10-1。

Image

图 10-1:员工 CSV 文件

图 10-1 是一个 Excel 截图,但你可以轻松地看到数据作为纯文本 CSV 文件的样子。对于我们的示例 CSV 文件,你将使用Employees.csv,它可以在本章的资源中找到;请参见清单 10-1。

PS> Get-Content -Path ./Employees.csv –Raw

First Name,Last Name,Department,Manager
Adam,Bertram,IT,Miranda Bertram
Barack,Obama,Executive Office,Michelle Obama
Miranda,Bertram,Executive Office
Michelle,Obama,Executive Office

清单 10-1:使用 Get-Content 读取 CSV 文件

在这里,你使用 Get-Content 命令来查询我们的文本文件(CSV)。Get-Content 是 PowerShell 中用于读取任何类型纯文本文件的命令。

你可以看到这是一个典型的 CSV 文件,包含一个头行和多个数据行,数据行通过逗号分隔成列。请注意,你可以通过使用Get-Content cmdlet 来读取该文件。由于 CSV 文件是一个文本文件,Get-Content能够很好地读取它(这实际上是Import-Csv执行的第一步)。

但也要注意Get-Content返回的信息:它是一个简单的字符串。这就是使用Raw参数时发生的情况。否则,Get-Content会返回一个字符串数组,每个元素代表 CSV 文件中的一行:

PS> Get-Content ./Employees.csv -Raw | Get-Member

   TypeName: System.String
   --snip--

尽管Get-Content命令可以读取数据,但该命令并不理解CSV 文件的结构。Get-Content不知道表格有头行或数据行,也不知道该如何处理分隔符。它只是获取内容然后输出。正因如此,我们才有了Import-Csv

使用 Import-Csv 处理数据

要查看Import-Csv的工作原理,可以将 Listing 10-1 中的输出与 Listing 10-2 中的Import-Csv输出进行对比。

PS> Import-Csv -Path ./Employees.csv

First Name Last Name  Department        Manager
---------- ---------  ----------        -------
Adam       Bertram    IT                Miranda Bertram
Barack     Obama      Executive Office  Michelle Obama
Miranda    Bertram    Executive Office
Michelle   Obama      Executive Office

PS> Import-Csv -Path ./Employees.csv | Get-Member

   TypeName: System.Management.Automation.PSCustomObject

PS> $firstCsvRow = Import-Csv -Path ./Employees.csv | Select-Object –First
1 
PS> $firstCsvRow | Select-Object -ExpandProperty 'First Name'
Adam

Listing 10-2: 使用 Import-Csv

你首先可能会注意到的是,标题与数据项之间现在由一条线分开。这意味着Import-Csv读取文件时,把顶部的行当作头行,并知道要将其与文件的其他部分分开。你还可能会注意到,没有更多的逗号——当一个命令读取并理解CSV 文件时,它知道分隔符用于分隔表格中的项,不应该出现在表格中。

但是如果代码中有一个多余的分隔符会发生什么呢?试着在Employees.csv文件中Adam的中间加入一个逗号,然后运行代码。会发生什么呢?现在 Adam 所在的行所有内容都被移动了:am变成了新的姓氏,Bertram变成了新的部门,IT变成了新的经理。Import-Csv足够智能,能够理解 CSV 的格式,但并不够聪明去理解其内容——这就是你要介入的地方。

将原始数据转换为对象

Import-Csv不仅仅是读取 CSV 并打印出带有花哨格式的数据。文件的内容被放入一个PSCustomObject数组中。在这里,每个PSCustomObject是一个保存一行数据的对象。每个对象都有与头行中的标题对应的属性,如果你想获取某个标题列的数据,只需要访问该属性即可。只要知道预期的数据形式,Import-Csv就能将它从一个从未见过的字符串数据转换成易于使用的对象。相当酷!

将数据作为PSCustomObject数组保存,允许你更有效地使用这些数据。假设你只想找到姓氏为Bertram的员工。由于 CSV 中的每一行数据都是一个PSCustomObject,你可以通过使用Where-Object来实现:

PS> Import-Csv -Path ./Employees.csv | Where-Object { $_.'Last Name' -eq 'Bertram' }

First Name Last Name Department       Manager
---------- --------- ----------       -------
Adam       Bertram   IT               Miranda Bertram
Miranda    Bertram   Executive Office

如果你想返回 CSV 中只有“行政办公室”部门的行,你也可以轻松实现!你只需使用相同的技巧,将属性名称从“Last Name”改为“Department”,并将值从“Bertram”改为“Executive Office”:

PS> Import-Csv -Path ./Employees.csv | Where-Object {$_.Department -eq 'Executive Office' }

First Name Last Name Department       Manager
---------- --------- ---------        --------
Barack     Obama     Executive Office Michelle Obama
Miranda    Bertram   Executive Office
Michelle   Obama     Executive Office

如果你使用分号作为分隔符而不是逗号,会发生什么?尝试更改 CSV 文件,看看会发生什么。不好吧?你不一定非得使用逗号作为分隔符,但逗号是Import-Csv原生理解的分隔符。如果你想使用其他分隔符,你需要在Import-Csv命令中指定新的分隔符。

为了演示,将我们Employees.csv文件中的所有逗号替换为制表符:

PS> (Get-Content ./Employees.csv -Raw).replace(',',"`t") | Set-Content ./Employees.csv
PS> Get-Content ./Employees.csv –Raw
First Name  Last Name   Department  Manager
Adam    Bertram IT  Miranda Bertram
Barack  Obama   Executive Office    Michelle Obama
Miranda Bertram Executive Office
Michelle    Obama   Executive Office

一旦你有了一个制表符分隔的文件,你就可以通过使用Delimiter参数(列表 10-3)指定制表符字符(由反引号和t字符表示)作为新的分隔符。

PS> Import-Csv -Path ./Employees.csv -Delimiter "`t"

First Name Last Name Department       Manager
---------- --------- ----------       -------
Adam       Bertram   IT               Miranda Bertram
Barack     Obama     Executive Office Michelle Obama
Miranda    Bertram   Executive Office
Michelle   Obama     Executive Office

列表 10-3:使用 Import-Csv 的 Delimiter 参数

请注意,输出与列表 10-2 中的相同。

定义你自己的表头

如果你有一个数据表,但想将表头行更改为更具用户友好的形式呢?Import-Csv也能做到这一点。与新的分隔符一样,你需要在 Import-Csv 中传递一个参数。列表 10-4 使用 Header 参数传入一个由逗号分隔的字符串序列(新的表头)。

PS> Import-Csv -Path ./Employees.csv -Delimiter "`t" 
-Header 'Employee FName','Employee LName','Dept','Manager'

Employee FName Employee LName Dept             Manager
-------------- -------------- ----             -------
First Name     Last Name      Department       Manager
Adam           Bertram        IT               Miranda Bertram
Barack         Obama          Executive Office Michelle Obama
Miranda        Bertram        Executive Office
Michelle       Obama          Executive Office

列表 10-4:使用 Import-Csv 的 Header 参数

如你所见,命令运行后,数据行中的每个对象都会将新标签作为属性名称。

创建 CSV 文件

至此,关于读取 CSV 文件的部分结束。如果你想自己制作一个 CSV 文件呢?你可以手动键入,但那会花费时间和精力,特别是当你处理成千上万行数据时。幸运的是,PowerShell 也有一个原生 cmdlet 来创建 CSV 文件:Export-Csv。你可以使用这个 cmdlet 从任何现有的 PowerShell 对象创建 CSV 文件;你只需告诉 PowerShell 使用哪些对象作为行,并指定它应将文件创建到何处。

让我们先处理第二个需求。假设你运行了一些 PowerShell 命令,然后你希望以某种方式将控制台中的输出保存到文件中。你可以使用Out-File,但那样会将未经结构化的文本直接发送到新文件中。你需要一个结构化良好的文件,包含表头行和分隔符。这时,Export-Csv就派上用场了。

举个例子,假设你想从计算机中获取所有正在运行的进程,并记录每个进程的名称、公司和描述。你可以使用 Get-Process 来做到这一点,并使用 Select-Object 来缩小你想查看的属性,如下所示:

PS> Get-Process | Select-Object -Property Name,Company,Description

Name                 Company                      Description
----                 -------                      -----------
ApplicationFrameHost Microsoft Corporation        Application Frame Host
coherence            Parallels International GmbH Parallels Coherence service
coherence            Parallels International GmbH Parallels Coherence service
coherence            Parallels International GmbH Parallels Coherence service
com.docker.proxy
com.docker.service   Docker Inc.                 
Docker.Service
--snip--

在列表 10-5 中,你可以看到当你以结构化方式将此输出提交到文件系统时,使用 Export-Csv 的结果。

PS> Get-Process | Select-Object -Property Name,Company,Description | 
Export-Csv -Path C:\Processes.csv –NoTypeInformation
PS> Get-Content -Path C:\Processes.csv
"Name","Company","Description"
"ApplicationFrameHost","Microsoft Corporation","Application Frame Host"
"coherence","Parallels International GmbH","Parallels Coherence service"
"coherence","Parallels International GmbH","Parallels Coherence service"
"coherence","Parallels International GmbH","Parallels Coherence service"
"com.docker.proxy",,
"com.docker.service","Docker Inc.","Docker.Service"

列表 10-5:使用 Export-Csv

通过将输出直接传输到Export-Csv,并指定你希望创建的 CSV 文件路径(使用Path参数),并使用NoTypeInformation参数,你已经创建了一个包含预期标题行和数据行的 CSV 文件。

注意

NoTypeInformation 参数不是必需的,但如果不使用它,你的 CSV 文件顶部会出现一行,指定该文件来自的对象类型。除非你将 CSV 文件直接重新导入到 PowerShell,否则通常不希望这样。一个示例行如下:#TYPE Selected.System.Diagnostics.Process.

项目 1:构建计算机库存报告

为了将到目前为止所学的内容整合起来,让我们做一个迷你项目,这是你在日常工作中可能会遇到的。

想象一下,你的公司收购了一家没有任何关于其网络上有哪些服务器和 PC 设备的知识的公司。它唯一拥有的就是一个包含 IP 地址和每个设备所在部门的 CSV 文件。你被招募进来,任务是弄清楚这些设备是什么,并将结果提供给管理层,创建一个新的 CSV 文件。

你需要做什么?从高层次来看,这是一个两步的过程:读取他们的 CSV 并写出你自己的 CSV。你的 CSV 文件需要包含以下信息:你处理的每个 IP 地址、它应该属于的部门、该 IP 地址是否响应 ping 请求,以及该设备的 DNS 名称。

你将从一个看起来像以下片段的 CSV 文件开始。IP 地址属于一个完整的 255.255.255.0 网络,因此它们的范围一直到 192.168.0.254:

PS> Get-Content -Path ./IPAddresses.csv
"192.168.0.1","IT"
"192.168.0.2","Accounting"
"192.168.0.3","HR"
"192.168.0.4","IT"
"192.168.0.5","Accounting"
--snip--

我创建了一个名为Discover-Computer.ps1的脚本,该脚本可以在本章的资源中找到。在进行这个实验时,可以开始向脚本中添加代码。

首先,你需要读取 CSV 文件中的每一行。你可以使用Import-Csv来实现,它将 CSV 的每一行捕获到一个变量中,以便进一步处理:

$rows = Import-Csv -Path C:\IPAddresses.csv

现在你有了数据,接下来需要使用它。你将对每个 IP 地址执行两个操作:ping 测试和查找其主机名。让我们先在 CSV 中的一行数据上测试这些操作,以确保语法正确。

在下面的列表中,你使用了Test-Connection命令,它向你指定的 IP 地址发送一个 ICMP 数据包(这里是 CSV 文件中第一行的 IP 地址)。Quiet参数告诉命令返回TrueFalse的值。

PS> Test-Connection -ComputerName $row[0].IPAddress -Count 1 –Quiet
PS> (Resolve-DnsName -Name $row[0].IPAddress -ErrorAction Stop).Name

在这段代码的第二行中,你通过对相同 IP 地址使用Resolve-DnsName命令来获取主机名。Resolve-DnsName命令返回多个属性。这里,因为你只关心名称,所以将整个命令括在括号内,并使用点符号返回Name属性。

一旦你熟悉了每个操作的语法,你需要对 CSV 中的每一行都执行此操作。最简单的方法是使用foreach循环:

foreach ($row in $rows) {
    Test-Connection -ComputerName $row.IPAddress -Count 1 –Quiet
    (Resolve-DnsName -Name $row.IPAddress -ErrorAction Stop).Name
}

亲自运行代码。会发生什么?你得到了一堆带有主机名的True/False行,但无法知道哪个 IP 地址与输出相关联。你需要为每一行创建一个哈希表并为其分配自己的元素。你还需要考虑Test-ConnectionResolve-DnsName返回错误的情况。清单 10-6 展示了如何做到这一点的示例。

$rows = Import-Csv -Path C:\IPAddresses.csv
foreach ($row in $rows) {
    try { ❶
        $output = @{ ❷
            IPAddress  = $row.IPAddress
            Department = $row.Department
            IsOnline   = $false
            HostName   = $null
            Error      = $null
        }
        if (Test-Connection -ComputerName $row.IPAddress -Count 1 -Quiet) { ❸
            $output.IsOnline = $true
        }
        if ($hostname = (Resolve-DnsName -Name $row.IPAddress -ErrorAction Stop).Name) { ❹
            $output.HostName = $hostName
        }
    } catch {
        $output.Error = $_.Exception.Message ❺
    } finally {
        [pscustomobject]$output ❻
    }
}

清单 10-6:迷你项目——CSV 文件发现

让我们逐步了解发生了什么。首先,你创建一个哈希表,包含与行的列对应的值以及你想要的额外信息❷。接下来,通过 ping 测试计算机是否连接,测试 IP 地址❸。如果计算机连接,设置IsOnlineTrue。然后,对HostName做同样的测试,检查它是否被找到❹,如果找到,则更新哈希表中的值。如果出现任何错误,将其记录在哈希表的Error值中❺。最后,将你的哈希表转换为PSCustomObject并返回它(无论是否抛出错误)❻。注意,你已将整个函数包裹在try/catch块中❶,如果try块中的代码抛出错误,则会执行catch块中的代码。因为你使用了ErrorAction参数,所以如果Resolve-DnsName遇到意外情况,它将抛出一个异常(错误)。

运行此命令后,你应该看到如下所示的输出:

HostName   :
Error      : 1.0.168.192.in-addr.arpa : DNS name does not exist
IsOnline   : True
IPAddress  : 192.168.0.1
Department : HR

HostName   :
Error      : 2.0.168.192.in-addr.arpa : DNS name does not exist
IsOnline   : True
IPAddress  : 192.168.0.2
Department : Accounting
--snip--

恭喜!你已经完成了大部分繁重的工作,现在你可以知道哪个 IP 地址与哪个输出相关联。剩下的就是将输出记录到 CSV 文件中。正如你之前所学的,你可以使用Export-Csv来做到这一点。你只需将你创建的PSCustomObject管道传递给Export-Csv,输出将直接写入 CSV 文件,而不是输出到控制台。

注意,接下来你将使用Append参数。默认情况下,Export-Csv会覆盖 CSV 文件。使用Append参数会将一行添加到现有 CSV 文件的末尾,而不是覆盖它:

PS> [pscustomobject]$output | 
Export-Csv -Path C:\DeviceDiscovery.csv -Append 
-NoTypeInformation

一旦脚本运行,你会看到 CSV 文件与在 PowerShell 控制台中看到的输出完全相同:

PS> Import-Csv -Path C:\DeviceDiscovery.csv

HostName   :
Error      : 1.0.168.192.in-addr.arpa : DNS name does not exist
IsOnline   : True
IPAddress  : 192.168.0.1
Department : HR

HostName   :
Error      :
IsOnline   : True
IPAddress  : 192.168.0.2
Department : Accounting

现在,你应该有一个名为DeviceDiscovery.csv(或你命名的任何名字)的 CSV 文件,文件中包含原始 CSV 中每个 IP 地址的行,以及你通过Test-ConnectionResolve-DnsName发现的所有原始 CSV 文件值和新发现的值。

Excel 电子表格

很难想象有哪个企业不使用 Excel 电子表格。如果你接到一个脚本编写项目,可能会涉及到一个 Excel 电子表格。但在深入了解 Excel 的世界之前,有一点需要明确地说明:如果可能,尽量不要使用它!

CSV 文件可以像简单的 Excel 电子表格一样有效地存储数据,并且使用 PowerShell 管理 CSV 文件要容易得多。Excel 电子表格是专有格式的文件,除非使用外部库,否则你无法直接用 PowerShell 读取它们。如果你有一个只有一个工作表的 Excel 工作簿,最好将其保存为 CSV 文件。当然,这并不总是可能的,但如果可以的话,以后你会感谢自己。相信我。

但如果无法将其保存为 CSV 呢?在这种情况下,你需要使用一个社区模块。曾几何时,使用 PowerShell 读取 .xls.xlsx 格式的 Excel 表格需要软件开发人员的精细操作。你必须安装 Excel,并且必须访问 COM 对象,这是一种复杂的编程组件,让 PowerShell 的工作变得不那么有趣。幸运的是,其他人已经为你做了繁重的工作,所以在本节中,你将依赖 Doug Finke 的精彩 ImportExcel 模块。这个免费的社区模块不需要安装 Excel,而且比 COM 对象简单得多。

首先,你需要安装该模块。ImportExcel模块可以通过 PowerShell Gallery 获取,可以通过运行 Install-Module ImportExcel 来安装。一旦安装了 ImportExcel 模块,你就可以开始查看它的功能了。

创建 Excel 电子表格

首先,你需要创建一个 Excel 电子表格。当然,你可以像平常一样打开 Excel 创建一个电子表格,但这样有趣吗?让我们使用 PowerShell 创建一个只有一个工作表的简单电子表格(在你学会走之前,得先学会爬行)。为此,你将使用 Export-Excel 命令。就像 Export-Csv 一样,Export-Excel 会读取它接收到的每个对象的属性名称,使用这些属性名称创建标题行,然后在下面创建数据行。

使用Export-Excel的最简单方法是将一个或多个对象传递给它,就像使用Export-Csv一样。举个例子,假设我们要创建一个包含我计算机上所有正在运行的进程的 Excel 工作簿,工作簿中只有一个工作表。

输入 Get-Process | Export-Excel .\Processes.xlsx 会生成一个像图 10-2 一样的电子表格。

Image

图 10-2:Excel 电子表格

如果你还没有将其转换为 CSV 格式,那么你可能正在处理比单一工作表更复杂的内容。让我们在现有的工作簿中再添加几个工作表。为此,请使用WorksheetName参数,如示例 10-7 所示。通过将发送到Export-Excel的对象,你可以创建额外的工作表。

PS> Get-Process | Export-Excel .\Processes.xlsx -WorksheetName 'Worksheet2'
PS> Get-Process | Export-Excel .\Processes.xlsx -WorksheetName 'Worksheet3'

示例 10-7:向 Excel 工作簿添加工作表

使用Export-Excel创建电子表格可能会复杂得多,但为了节省时间(并为地球节省几棵树),我们在这里就不详细讨论了。如果你感兴趣,可以查阅Export-Excel的帮助文档,你会看到可以使用的众多参数!

读取 Excel 电子表格

现在你已经有了一个可以操作的电子表格,让我们集中讨论如何读取其中的行。要读取电子表格,你使用Import-Excel命令。该命令读取工作簿中的一个工作表,并返回一个或多个代表每一行的PSCustomObject对象。使用这个命令的最简单方法是通过Path参数指定工作簿的路径。你会在示例 10-8 中看到,Import-Excel返回一个使用列名作为属性的对象。

PS> Import-Excel -Path .\Processes.xlsx

Name                       : ApplicationFrameHost
SI                         : 1
Handles                    : 315
VM                         : 2199189057536
WS                         : 26300416
PM                         : 7204864
NPM                        : 17672
Path                       : C:\WINDOWS\system32\ApplicationFrameHost.exe
Company                    : Microsoft Corporation
CPU                        : 0.140625
--snip--

示例 10-8:使用 Import-Excel

默认情况下,Import-Excel将只返回第一个工作表。我们的示例工作簿有多个工作表,因此你需要找出一种方法来遍历每个工作表。但假设自从你最后创建这个电子表格以来已经有一段时间了,你记不住工作表的名称了。没问题。你可以使用Get-ExcelSheetInfo来查找工作簿中的所有工作表,如示例 10-9 所示。

PS> Get-ExcelSheetInfo -Path .\Processes.xlsx

Name       Index  Hidden  Path
----       -----  ------  ----
Sheet1         1  Visible C:\Users\adam\Processes.xlsx
Worksheet2     2  Visible C:\Users\adam\Processes.xlsx
Worksheet3     3  Visible C:\Users\adam\Processes.xlsx

示例 10-9:使用 Get-ExcelSheetInfo

你将使用这个输出从所有工作表中提取数据。创建一个foreach循环,并对工作簿中的每个工作表调用Import-Excel,就像在示例 10-10 中一样。

$excelSheets = Get-ExcelSheetInfo -Path .\Processes.xlsx
Foreach ($sheet in $excelSheets) {
 $workSheetName = $sheet.Name
 $sheetRows = Import-Excel -Path .\Processes.xlsx -WorkSheetName
 $workSheetName 
  ❶ $sheetRows | Select-Object -Property *,@{'Name'='Worksheet';'Expression'={ $workSheetName }
}

示例 10-10:从所有工作表获取所有行

请注意,你在Select-Object ❶中使用了一个计算属性。通常,在使用Select-ObjectProperty参数时,使用简单的字符串来指定你想返回的属性。但如果使用计算属性,你需要为Select-Object提供一个包含返回属性名称和在接收输入时运行的表达式的哈希表。表达式的结果将是新计算属性的值。

默认情况下,Import-Excel不会将工作表名称作为属性添加到每个对象中——这意味着你无法知道行来自哪个工作表。为了解决这个问题,你需要在每一行对象上创建一个名为Worksheet的属性,以便后续引用。

向 Excel 电子表格添加数据

在上一节中,你从头开始创建了一个工作簿。总会有那么一刻,你需要向工作表添加行。幸运的是,使用ImportExcel模块这非常简单;你只需要在Export-Excel命令中使用Append参数即可。

举个例子,假设你想跟踪你计算机上的进程执行历史。你希望导出你计算机上在一段时间内运行的所有进程,并在 Excel 中对结果进行比较。为此,你需要导出所有正在运行的进程,并确保在每行中包含一个时间戳,表示收集进程信息的时间。

让我们在演示工作簿中添加另一个工作表,并将其命名为ProcessesOverTime。你将使用计算属性为每个进程行添加一个时间戳属性,像这样:

PS> Get-Process | 
Select-Object -Property *,@{Name = 'Timestamp';Expression = { Get-Date -Format
'MM-dd-yy hh:mm:ss' }} | 
Export-Excel .\Processes.xlsx -WorksheetName 'ProcessesOverTime'

运行此命令后,打开进程工作簿。你应该能看到一个名为 ProcessesOverTime 的工作表,列出计算机上所有运行的进程,并且还有一个附加的时间戳列,指示进程信息被查询的时间。

此时,你将通过使用刚才使用的相同命令,但这次加上Append参数,向工作表附加更多行。这个命令可以执行多次,每次都会将行附加到工作表中:

PS> Get-Process | 
Select-Object -Property *,@{Name = 'Timestamp';Expression = { Get-Date -Format
'MM-dd-yy hh:mm:ss' }} | 
Export-Excel .\Processes.xlsx -WorksheetName 'ProcessesOverTime' -Append

一旦收集了数据,你可以查看你的 Excel 工作簿以及所有收集到的进程信息。

项目 2:创建一个 Windows 服务监控工具

让我们将你在本节中学到的技能结合起来,进行另一个迷你项目。这一次,你将构建一个进程来跟踪 Windows 服务的状态变化并将其记录到 Excel 工作表中。然后,你将生成一份报告,展示各个服务何时发生状态变化——基本上,你在制作一个低成本的监控工具。

首先,你需要弄清楚如何拉取所有 Windows 服务,并仅返回它们的名称和状态。你可以通过运行Get-Service | Select-Object -Property Name,Status来轻松实现这一点。接下来,你需要在每一行 Excel 工作表中添加一个时间戳。就像在课程中一样,你将使用计算属性来实现这一点;见清单 10-11。

PS> Get-Service | 
Select-Object -Property Name,Status,@{Name = 'Timestamp';Expression = { Get-Date -Format 'MM-dd-yy hh:mm:ss' }} | 
Export-Excel .\ServiceStates.xlsx -WorksheetName 'Services'

清单 10-11:导出服务状态

你现在应该已经创建了一个名为ServiceStates.xlsx的 Excel 工作簿,里面有一个名为 Services 的工作表,内容大致如下所示:图 10-3。

Image

图 10-3:Excel 工作簿

在再次运行相同的命令之前,让我们更改一些 Windows 服务的状态。这将允许你跟踪状态的变化。停止并启动一些服务以更改它们的状态。然后像在清单 10-11 中一样运行相同的命令,不过这次使用Append参数进行Export-Excel。这将为你提供一些可以使用的数据。(别忘了使用Append参数,否则命令会覆盖现有的工作表!)

收集到数据后,是时候对其进行汇总了。Excel 提供了多种方法来实现这一点,但现在你将使用透视表。透视表是一种通过将一个或多个属性分组,然后对这些属性的对应值执行某些操作(如计数、求和等)来汇总数据的方法。使用透视表,你可以轻松查看哪些服务发生了状态变化,以及它们何时发生变化。

你将使用IncludePivotTablePivotRowsPivotColumnsPivotData参数来创建一个汇总透视表(见图 10-4)。

Image

图 10-4:服务状态透视表

正如你在清单 10-12 中看到的,你正在读取 Services 工作表中的数据,并使用这些数据创建数据透视表。

PS> Import-Excel .\ServiceStates.xlsx -WorksheetName 'Services' | 
Export-Excel -Path .\ServiceStates.xlsx -Show -IncludePivotTable -PivotRows Name,Timestamp
-PivotData @{Timestamp = 'count'} -PivotColumns Status

清单 10-12:使用 PowerShell 创建 Excel 数据透视表

ImportExcel PowerShell 模块提供了一系列选项供你使用。如果你想继续使用这个数据集,随意操作并查看你能做些什么。看看 ImportExcel GitHub 仓库(https://github.com/dfinke/ImportExcel),或者如果你想使用其他数据,尝试一下。只要你有数据,PowerShell 就能以几乎任何你喜欢的方式进行操作和表示!

JSON 数据

如果你在过去五年里一直从事技术工作,你可能已经读过一些 JSON。JSON 是在 2000 年代初期创建的,JavaScript 对象表示法(JSON)是一种机器可读、人类可理解的语言,用于表示层级数据集。正如它的名字所示,它在 JavaScript 应用程序中被广泛使用,这意味着它在 web 开发中占有重要地位。

最近,使用 REST API(一种用于在客户端和服务器之间传输数据的技术)在线服务的数量激增,导致了 JSON 使用量的激增。如果你从事任何与 web 相关的工作,JSON 是一个值得了解的格式,而且在 PowerShell 中也很容易管理。

读取 JSON

类似于读取 CSV,PowerShell 中读取 JSON 的方式有几种:有解析和无解析。由于 JSON 本质上是纯文本,PowerShell 默认将其视为字符串。例如,看看本章资源中找到的 JSON 文件 Employees.json,这里重新展示一下:

{
    "Employees": [
        {
            "FirstName": "Adam",
            "LastName": "Bertram",
            "Department": "IT",
            "Title": "Awesome IT Professional"
        },
        {
            "FirstName": "Bob",
            "LastName": "Smith",
            "Department": "HR",
            "Title": "Crotchety HR guy"
        }
    ]
}

如果你只需要字符串输出,可以使用 Get-Content -Path Employees``.json -Raw 来读取文件并返回字符串。但你不能仅用字符串做太多事。你需要结构化的数据。为了得到这些,你需要一些能够理解 JSON 模式(即各个节点和节点数组在 JSON 中如何表示)并能据此解析文件的工具。你需要 ConvertFrom-Json cmdlet。

ConvertFrom-Json cmdlet 是 PowerShell 中的一个原生命令,它接受原始的 JSON 输入,并将其转换为 PowerShell 对象。你可以在清单 10-13 中看到,PowerShell 现在知道 Employees 是一个属性。

PS> Get-Content -Path .\Employees.json -Raw | ConvertFrom-Json

Employees
---------
{@{FirstName=Adam; LastName=Bertram; Department=IT;
Title=Awesome IT Professional}, @{FirstName=Bob;
LastName=Smith; Department=HR; Title=Crotchety H...

清单 10-13:将 JSON 转换为对象

如果你查看 Employees 属性,你会看到所有员工节点都已被解析出来,每个键代表一列标题,每个值代表行的值:

PS> (Get-Content -Path .\Employees.json -Raw | ConvertFrom-Json).Employees

FirstName LastName Department Title
--------- -------- ---------- -----
Adam      Bertram  IT         Awesome IT Professional
Bob       Smith    HR         Crotchety HR guy

Employees 属性现在是一个对象数组,你可以像查询和操作任何其他数组一样,查询和操作它。

创建 JSON 字符串

假设你有来自多个来源的大量数据,并且想要将它们全部转换为 JSON。你该怎么办?这就是 ConvertTo-Json cmdlet 的神奇之处:它可以将 PowerShell 中的任何对象转换为 JSON。

例如,让我们将你在本章前面构建的 CSV 文件转换为Employees.json。首先,你需要导入我们的 CSV:

PS> Import-Csv -Path .\Employees.csv -Delimiter "`t"

First Name Last Name Department       Manager
---------- --------- ----------       -------
Adam       Bertram   IT               Miranda Bertram
Barack     Obama     Executive Office Michelle Obama
Miranda    Bertram   Executive Office
Michelle   Obama     Executive Office

要进行转换,你需要将输出通过管道传递给ConvertTo-Json,如清单 10-14 所示。

PS> Import-Csv -Path .\Employees.csv -Delimiter "`t" | ConvertTo-Json
[
    {
        "First Name":  "Adam",
        "Last Name":  "Bertram",
        "Department":  "IT",
        "Manager":  "Miranda Bertram"
    },
    {
        "First Name":  "Barack",
        "Last Name":  "Obama",
        "Department":  "Executive Office",
        "Manager":  "Michelle Obama"
    },
    {
        "First Name":  "Miranda",
        "Last Name":  "Bertram",
        "Department":  "Executive Office",
        "Manager":  null
    },
    {
        "First Name":  "Michelle",
        "Last Name":  "Obama",
        "Department":  "Executive Office",
        "Manager":  null
    }
]

清单 10-14:将对象转换为 JSON

正如你现在可能预料到的那样,你可以传递几个参数来修改转换。一项有用的参数是Compress,它通过去除所有可能不需要的换行符来缩小输出:

PS> Import-Csv -Path .\Employees.csv -Delimiter "`t" | ConvertTo-Json –Compress
[{"First Name":"Adam","Last
Name":"Bertram","Department":"IT","Manager":"Miranda
Bertram"},{"First Name":"Barack","Last
Name":"Obama","Department":"Executive Office","Manager":"Michelle Obama"},{"First
Name":"Miranda","Last Name":"Bertram","Department":"Executive
Office","Manager":null},{"First Name":"Michelle",
"Last Name":"Obama","Department":"Executive
Office","Manager":null}]

如果它有一个属性和值,ConvertTo-Json就可以完成它的工作。属性总是节点的键,属性值总是节点的值。

项目 3:查询和解析 REST API

现在你知道如何解析 JSON 了,接下来我们做一些更高级的操作:使用 PowerShell 查询一个 REST API 并解析结果。你可以使用几乎任何 REST API,但有些需要身份验证,因此不涉及这些额外步骤会更容易。我们使用一个不需要身份验证的 API。我找到一个不需要身份验证的 REST API,它来自postcodes.io,这是一个允许你根据不同标准查询英国邮政编码的服务。

你将使用的 URI 是* api.postcodes.io/random/postcodes。当你访问这个 URI 时,它会查询postcodes.io* API 服务,并返回一个随机的邮政编码,格式为 JSON。要查询此 URI,你将使用 PowerShell 的Invoke-WebRequest cmdlet:

PS> $result = Invoke-WebRequest -Uri 'http://api.postcodes.io/random/postcodes'
PS> $result.Content
{"status":200,"result":{"postcode":"IP12
2FE","quality":1,"eastings":641878,"northings":250383,"country
:"England","nhs_ha":"East of England","longitude":
1.53013518866685,"latitude":52.0988661618569,"european_elector
al_region":"Eastern","primary_care_trust":"Suffolk","region":"
East of England","lsoa":"Suffo
lk Coastal 007C","msoa":"Suffolk Coastal
007","incode":"2FE","outcode":"IP12","parliamentary_constituen
cy":"Suffolk Coastal","admin_district":"Suffolk Coa
stal","parish":"Orford","admin_county":"Suffolk","admin_ward":
"Orford & Eyke","ccg":"NHS Ipswich and East
Suffolk","nuts":"Suffolk","codes":{"admin_distri
ct":"E07000205","admin_county":"E10000029","admin_ward":"E0501
449","parish":"E04009440","parliamentary_constituency":"E14000
81","ccg":"E38000086","nuts"
:"UKH14"}}}

在 Windows PowerShell 中,Invoke-WebRequest依赖于 Internet Explorer。如果你的计算机上没有 Internet Explorer,你可能需要使用-UseBasicParsing参数来去除该依赖。 “高级”解析会稍微详细地拆解结果的 HTML 输出,但并不是所有情况下都需要。

现在,让我们看看你能否将结果转换为 PowerShell 对象:

PS> $result = Invoke-WebRequest -Uri 'http://api.postcodes.io/random/postcodes'
PS> $result.Content | ConvertFrom-Json

status result
------ ------
   200 @{postcode=DE7 9HY; quality=1; eastings=445564;
       northings=343166; country=England; nhs_ha=East Midlands;
       longitude=-1.32277519314161; latitude=...

PS> $result = Invoke-WebRequest -Uri 'http://api.postcodes.io/random/postcodes'
PS> $contentObject = $result.Content | ConvertFrom-Json
PS> $contentObject.result

postcode                   : HA7 2SR
quality                    : 1
eastings                   : 516924
northings                  : 191681
country                    : England
nhs_ha                     : London
longitude                  : -0.312779792807334
latitude                   : 51.6118279308721
european_electoral_region  : London
primary_care_trust         : Harrow
region                     : London
lsoa                       : Harrow 003C
msoa                       : Harrow 003
incode                     : 2SR
outcode                    : HA7
parliamentary_constituency : Harrow East
admin_district             : Harrow
parish                     : Harrow, unparished area
admin_county               :
admin_ward                 : Stanmore Park
ccg                        : NHS Harrow
nuts                       : Harrow and Hillingdon
codes                      : @{admin_district=E09000015;
                             admin_county=E99999999; admin_ward=E05000303;
                             parish=E43000205;

你可以毫无问题地将响应转换为 JSON。但你必须使用两个命令,Invoke-WebRequestConvertFrom-Json。如果你只需要使用一个命令,生活是不是会更美好?事实证明,PowerShell 有一个命令可以为你完成所有操作:Invoke-RestMethod

Invoke-RestMethod cmdlet 类似于Invoke-WebRequest;它将各种 HTTP 请求方法发送到 Web 服务并返回响应。由于postcodes.io API 服务不需要任何身份验证,你可以直接在Invoke-RestMethod中使用Uri参数来获取 API 响应:

PS> Invoke-RestMethod –Uri 'http://api.postcodes.io/random/postcodes'

status result
------ ------
   200 @{postcode=NE23 6AA; quality=1; eastings=426492;
       northings=576264; country=England; nhs_ha=North East;
       longitude=-1.5865793029774; latitude=55...

你可以看到,Invoke-RestMethod返回了 HTTP 状态代码和来自 API 的响应,在result属性中。所以,JSON 在哪里呢?如你所愿,它已经被转换为对象了。你无需手动将 JSON 转换为对象,因为你可以直接使用result属性:

PS> (Invoke-RestMethod –Uri 'http://api.postcodes.io/random/postcodes').result

postcode                   : SY11 4BL
quality                    : 1
eastings                   : 332201
northings                  : 331090
country                    : England
nhs_ha                     : West Midlands
longitude                  : -3.00873643515338
latitude                   : 52.8729967314029
european_electoral_region  : West Midlands
primary_care_trust         : Shropshire County
region                     : West Midlands
lsoa                       : Shropshire 011E
msoa                       : Shropshire 011
incode                     : 4BL
outcode                    : SY11
parliamentary_constituency : North Shropshire
admin_district             : Shropshire
parish                     : Whittington
admin_county               :
admin_ward                 : Whittington
ccg                        : NHS Shropshire
nuts                       : Shropshire CC
codes                      : @{admin_district=E06000051;
                             admin_county=E99999999; admin_ward=E05009287;
                             parish=E04012256;

在 PowerShell 中处理 JSON 是一个直接的过程。借助 PowerShell 易于使用的 cmdlet,通常无需复杂的字符串解析——只需将 JSON 或即将转化为 JSON 的对象传入管道,然后看着魔法发生!

总结

本章介绍了几种数据结构的构建方式,以及如何在 PowerShell 中处理这些结构。PowerShell 原生的 cmdlet 使得这个过程变得轻松,它抽象了许多复杂的代码,提供了简单易用的命令。但不要让它的简洁性让你掉以轻心:PowerShell 可以解析和操作几乎任何类型的数据。即便它没有原生命令来处理某种数据类型,由于基于 .NET 框架,它也能够深入到任何 .NET 类中,处理任何高级概念。

在下一章,我们将学习微软的活动目录(AD)。活动目录充满了重复的任务,是学习使用 PowerShell 时常见的起点;在本书的其余部分,我们将花费大量时间在这一重要资源上。

第十一章:自动化 Active Directory

图片

使用 PowerShell 自动化的最佳产品之一就是微软的 Active Directory(AD)。员工不断地进出并在组织中调动。需要一个动态系统来跟踪员工的不断变化,而这正是 AD 的作用。IT 专业人员在 AD 中执行重复且相似的任务,这使得它成为自动化的理想场所。

在本章中,我们将演示如何使用 PowerShell 自动化处理一些涉及 AD 的场景。虽然可以使用 PowerShell 操作许多 AD 对象,但我们只会涉及三种最常见的对象:用户账户、计算机账户和组。这些对象是 AD 管理员日常工作中最常遇到的。

前提条件

当你跟随本章的示例进行操作时,我假设你的计算机环境符合一些基本条件。

第一个要求是你正在使用一台已经是 Active Directory 域成员的 Windows 计算机。虽然有方法可以通过使用备用凭据从工作组计算机操作 AD,但这超出了本章的范围。

第二个要求是你将使用与你的计算机属于同一域的环境。复杂的跨域和林信任问题也超出了本章的范围。

最后,你需要确保使用的是具有适当权限的 AD 账户登录到计算机,以便读取、修改和创建常见的 AD 对象,如用户、计算机、组和组织单位。我是在一个属于域管理员组的账户下进行这些练习的——这意味着我对我的域中的所有内容都有控制权限。虽然这不是完全必要的,通常也不推荐在生产环境中使用,但这使我可以在不担心对象权限的情况下演示各种主题,而对象权限超出了本书的范围。

安装 ActiveDirectory PowerShell 模块

正如你现在所知道的,使用 PowerShell 完成任务有不止一种方式。同样,当你可以利用现有工具来构建更大、更好的工具时,就没有必要重新发明轮子。在本章中,你将只使用一个模块:ActiveDirectory。尽管它有一些不足之处——不太直观的参数、奇怪的过滤语法、异常的错误行为——但它无疑是管理 AD 最全面的模块。

ActiveDirectory模块随远程服务器管理工具软件包提供。该软件包包含许多工具,而且不幸的是,在撰写本文时,这是获取ActiveDirectory模块的唯一方式。在继续阅读本章之前,我建议你下载并安装此软件包。安装后,你将拥有ActiveDirectory模块。

为了确认你已经安装了ActiveDirectory,可以使用Get-Module命令:

PS> Get-Module -Name ActiveDirectory -List  
Directory: C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules

ModuleType  Version  Name             ExportedCommands
----------  -------  ----             ----------------
Manifest    1.0.0.0  ActiveDirectory  {Add-ADCentralAccessPolicyMember,...

如果你看到此输出,说明ActiveDirectory已经安装。

查询和过滤 AD 对象

一旦你确保已经满足所有前提条件并安装了 ActiveDirectory 模块,你就可以开始了。

适应新的 PowerShell 模块的最佳方法之一是查找所有以 Get 为动词的命令。以 Get 开头的命令仅用于读取信息,因此你意外更改某些内容的风险较小。我们将采取这种方法,使用 ActiveDirectory 模块,查找与本章中将要操作的对象相关的命令。Listing 11-1 展示了如何仅检索那些以 Get 开头并且动词部分包含 computerActiveDirectory 命令。

PS> Get-Command -Module ActiveDirectory -Verb Get -Noun *computer*

CommandType     Name                               Version    Source
-----------     ----                               -------    ------
Cmdlet          Get-ADComputer                     1.0.0.0    ActiveDirectory
Cmdlet          Get-ADComputerServiceAccount       1.0.0.0    ActiveDirectory

PS> Get-Command -Module ActiveDirectory -Verb Get -Noun *user*

CommandType     Name                               Version    Source
-----------     ----                               -------    ------
Cmdlet          Get-ADUser                         1.0.0.0    ActiveDirectory
Cmdlet          Get-ADUserResultantPasswordPolicy  1.0.0.0    ActiveDirectory

PS> Get-Command -Module ActiveDirectory -Verb Get -Noun *group*

CommandType     Name                               Version    Source
-----------     ----                               -------    ------
Cmdlet          Get-ADAccountAuthorizationGroup    1.0.0.0    ActiveDirectory
Cmdlet          Get-ADGroup                        1.0.0.0    ActiveDirectory
Cmdlet          Get-ADGroupMember                  1.0.0.0    ActiveDirectory
Cmdlet          Get-ADPrincipalGroupMembership     1.0.0.0    ActiveDirectory

Listing 11-1: ActiveDirectory 模块 Get 命令

你可以看到一些看起来很有趣的命令。在本章中,你将使用 Get-ADComputerGet-ADUserGet-ADGroupmGet-ADGroupMember 命令。

过滤对象

你将使用的许多 Get AD 命令都有一个名为 Filter 的公共参数。Filter 类似于 PowerShell 的 Where-Object 命令,因为它过滤每个命令返回的内容,但在实现这一任务的方式上有所不同。

Filter 参数使用它自己的语法,并且在使用复杂的过滤器时可能会很难理解。要详细了解 Filter 参数的语法,你可以运行 Get-Help about_ActiveDirectory_Filter

在本章中,我们将保持简单,避免使用任何高级过滤。首先,让我们使用 Filter 参数和 Get-ADUser 命令返回域中的所有用户,如 Listing 11-2 所示。不过要小心:如果你的域中有大量用户账户,可能需要等待一段时间。

PS> Get-ADUser -Filter *

DistinguishedName : CN=adam,CN=Users,DC=lab,DC=local
Enabled           : True
GivenName         :
Name              : adam
ObjectClass       : user
ObjectGUID        : 5e53c562-4fd8-4620-950b-aad8fbaa84db
SamAccountName    : adam
SID               : S-1-5-21-930245869-402111599-3553179568-500
Surname           :
UserPrincipalName :
--snip--

Listing 11-2: 查找域中的所有用户账户

如你所见,Filter 参数接受一个字符串值通配符字符 *。单独使用时,这个字符告诉(大多数)Get 命令返回它们找到的所有内容。尽管这种做法偶尔会有用,但大多数时候你并不想要所有可能的对象。不过,如果正确使用,通配符字符是一个强大的工具。

假设你想在 AD 中查找所有以字母 C 开头的计算机账户。你可以通过运行 Get-ADComputer -Filter 'Name -like "C*"' 来实现,其中 C* 代表所有以 C 开头的字符。你也可以反过来操作;假设你想查找姓氏以 son 结尾的人。你可以运行命令 Get-ADComputer -Filter 'Name -like "*son"'

如果你想找到所有姓Jones的用户,可以运行Get-ADUser -Filter "surName -eq 'Jones'";如果你想根据名字和姓氏找到一个用户,可以运行Get-ADUser -Filter "surName -eq 'Jones' -and givenName -eq 'Joe'"Filter参数允许你使用各种 PowerShell 操作符,如likeeq,构建一个仅返回你所需要结果的过滤器。Active Directory 属性以小驼峰命名法存储在 AD 数据库中,因此在过滤器中使用的是这种格式,尽管从技术上讲,这并不是必须的。

另一个用于过滤 AD 对象的有用命令是Search-ADAccount命令。该命令内置了对常见过滤场景的支持,比如查找密码已过期的所有用户、查找被锁定的用户,以及查找已启用的计算机。查看Search-ADAccount cmdlet 的帮助文档,了解所有参数。

大多数情况下,Search-ADAccount语法是自解释的。各种切换参数,包括PasswordNeverExpiresAccountDisabledAccountExpired,不需要其他参数即可使用。

除了这些高级参数,Search-ADAccount还具有一些需要额外输入的参数——例如,指示日期时间属性的年龄,或者如果你需要按特定对象类型(例如,用户或计算机)限制结果。

让我们以AccountInactive参数为例。假设你想查找 90 天内没有使用其账户的所有用户。这是Search-ADAccount的一个很好的查询。通过使用示例 11-3 中的语法,使用UsersOnly来过滤对象类型,并使用TimeSpan来过滤过去 90 天内未活跃的对象,你可以快速找到所有符合要求的用户。

PS> Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly

示例 11-3:使用Search-ADAccount

Search-ADAccount cmdlet 返回的对象类型是Microsoft.ActiveDirectory.Management.ADUser。这是与Get-ADUserGet-ADComputer等命令返回的对象类型相同的类型。当你使用Get命令并感到卡住,不知道该如何编写Filter参数的语法时,Search-ADAccount可以作为一个很好的快捷方式。

返回单个对象

有时你知道自己要查找的确切 AD 对象,因此根本不需要使用Filter。在这种情况下,你可以使用Identity参数。

Identity是一个灵活的参数,允许你指定使 AD 对象唯一的属性;因此,它只会返回一个对象。每个用户帐户都有一个唯一的属性,叫做samAccountName。你可以使用Filter参数查找所有具有特定samAccountName的用户,语法如下:

Get-ADUser -Filter "samAccountName -eq 'jjones'"

但是使用Identity参数会更加简洁:

Get-ADUser -Identity jjones

项目 4:查找 30 天内没有更改密码的用户帐户

现在你对如何查询 AD 对象有了基本了解,接下来让我们创建一个小脚本并将这些知识付诸实践。场景是这样的:你在一家公司工作,公司即将实施新的密码过期政策,而你的工作是找到过去 30 天内未更改密码的所有账户。

首先,让我们考虑使用什么命令。你可能首先想到的是本章前面学到的Search-ADAccount命令。Search-ADAccount有很多用途,用于搜索和筛选各种对象,但你无法创建自定义筛选器。为了更精细地进行搜索,你需要使用Get-ADUser命令来构建自己的筛选器。

一旦你知道将使用什么命令,下一步就是确定要筛选的内容。你知道你要筛选出过去 30 天内没有更改密码的账户,但如果你只查找这一点,你会找到比实际需要更多的账户。为什么?如果你不筛选出Enabled账户,你可能会得到一些已经不再重要的旧账户(例如离开公司或失去计算机权限的人)。因此,你需要查找那些过去 30 天内未更改密码的启用计算机账户。

让我们从筛选启用的用户账户开始。你可以通过使用–Filter "Enabled -eq 'True'"来做到这一点。很简单。下一步是找出当用户的密码设置时存储的属性。

默认情况下,Get-ADUser不会返回用户的所有属性。通过使用Properties参数,你可以指定希望查看的属性;在这里,你将使用namepasswordlastset。请注意,有些用户没有passwordlastset属性,这是因为他们从未设置过自己的密码。

PS> Get-AdUser -Filter * -Properties passwordlastset  | select name,passwordlastset

name           passwordlastset
----           ---------------
adam           2/22/2019 6:45:40 AM
Guest
DefaultAccount
krbtgt         2/22/2019 3:03:32 PM
Non-Priv User  2/22/2019 3:12:38 PM
abertram
abertram2
fbar
--snip--

现在你已经有了属性名称,你需要为其构建一个筛选器。记住,你只想筛选那些在过去 30 天内更改了密码的账户。为了找到日期差,你需要两个日期:最早的日期(30 天前)和最新的日期(今天)。你可以通过使用Get-Date命令轻松获得今天的日期。然后可以使用AddDays方法来计算 30 天前的日期。你会将两个日期存储在变量中,方便以后访问。

PS> $today = Get-Date
PS> $30DaysAgo = $today.AddDays(-30)

现在你已经有了日期,可以在筛选器中使用它们:

PS> Get-ADUser -Filter "passwordlastset -lt '$30DaysAgo'"

剩下的就是将你的Enabled条件添加到过滤器中。列表 11-4 展示了执行此操作的步骤。

$today = Get-Date
$30DaysAgo = $today.AddDays(-30)
Get-ADUser -Filter "Enabled -eq 'True' -and passwordlastset –lt
'$30DaysAgo'"

列表 11-4:查找过去 30 天内未更改密码的启用用户账户

现在你已经编写了一些代码,用于查找所有在过去 30 天内已设置密码的启用 Active Directory 用户。

创建和更改 AD 对象

现在你已经知道如何查找现有的 AD 对象,让我们来学习如何更改和创建它们。本节分为两部分:一部分涉及用户和计算机,另一部分涉及组。

用户和计算机

要更改用户和计算机账户,你将使用 Set 命令:Set-ADUserSet-ADComputer。这些命令可以更改对象的任何属性。通常,你会希望将从 Get 命令(如上一节中介绍的命令)获取的对象传递给它们。

作为一个例子,假设一名员工名叫 Jane Jones,她结婚了,你需要更改她用户账户的姓氏。如果你不知道此用户账户的身份属性,你可以在 Get-ADUser 上使用 Filter 参数来查找它。但首先,你需要发现 AD 是如何存储每个用户的名字和姓氏的。然后,你可以使用这些属性的值传递给 Filter 参数。

查找存储在 AD 中的所有可用属性的一种方法是使用一些.NET 代码。通过使用模式对象,你可以找到用户类并枚举其所有属性:

$schema =[DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$userClass = $schema.FindClass('user')
$userClass.GetAllProperties().Name

通过查看可用属性列表,你会找到 givenNamesurName 属性,这些属性可以与 Get-ADUser 命令中的 Filter 参数一起使用,找到用户账户。接下来,你可以将该对象传递给 Set-ADUser,如 列表 11-5 所示。

PS> Get-ADUser -Filter "givenName -eq 'Jane' -and surName –eq
'Jones'" | Set-ADUser -Surname 'Smith'
PS> Get-ADUser -Filter "givenName -eq 'Jane' -and surName –eq
'Smith'"

DistinguishedName : CN=jjones,CN=Users,DC=lab,DC=local
Enabled           : False
GivenName         : Jane
Name              : jjones
ObjectClass       : user
ObjectGUID        : fbddbd77-ac35-4664-899c-0683c6ce8457
SamAccountName    : jjones
SID               : S-1-5-21-930245869-402111599-3553179568-3103
Surname           : Smith
UserPrincipalName :

列表 11-5:使用 Set-ADUser 更改 AD 对象属性

你还可以一次更改多个属性。结果发现 Jane 也调动了部门并且得到了晋升,这两个变动都需要更新。没问题,你只需要使用与 AD 属性相匹配的参数:

PS> Get-ADUser -Filter "givenName -eq 'Jane' -and surname –eq
'Smith'" | Set-ADUser -Department 'HR' -Title Director
PS> Get-ADUser -Filter "givenName -eq 'Jane' -and surname –eq
'Smith'" -Properties GivenName,SurName,Department,Title

Department        : HR
DistinguishedName : CN=jjones,CN=Users,DC=lab,DC=local
Enabled           : False
GivenName         : Jane
Name              : jjones
ObjectClass       : user
ObjectGUID        : fbddbd77-ac35-4664-899c-0683c6ce8457
SamAccountName    : jjones
SID               : S-1-5-21-930245869-402111599-3553179568-3103
Surname           : Smith
Title             : Director
UserPrincipalName :

最后,你可以使用 New-AD* 命令创建 AD 对象。创建新的 AD 对象与更改现有对象类似,但在这里你无法使用 Identity 参数。创建一个新的 AD 计算机账户就像运行 New-ADComputer -Name FOO 一样简单;同样,可以通过使用 New-ADUser -Name adam 创建一个 AD 用户。你会发现 New-AD* 命令也有与 AD 属性相关的参数,和 Set-AD* 命令一样。

比用户和计算机更复杂。你可以把组看作是许多 AD 对象的容器。从这个意义上来说,组就是一堆东西。但同时,它仍然是一个单一的容器,意味着像用户和计算机一样,组是一个单一的 AD 对象。这也意味着你可以像查询、创建和更改用户和计算机一样查询、创建和更改组,尽管会有一些细微的差别。

也许你的组织创建了一个新的部门,叫做 AdamBertramLovers,它正在快速扩张,吸引了很多新员工。现在你需要创建一个名为该部门的组。列表 11-6 显示了如何创建这样的组的示例。你使用 Description 参数传入一个字符串(组的描述),并使用 GroupScope 参数来确保创建的组具有 DomainLocal 范围。如果需要的话,你也可以选择 GlobalUniversal

PS> New-ADGroup -Name 'AdamBertramLovers' 
-Description 'All Adam Bertram lovers in the company' 
-GroupScope DomainLocal

列表 11-6:创建一个 AD 组

一旦组存在,你可以像修改用户或计算机一样修改它。例如,如果要更改描述,你可以这样做:

PS> Get-ADGroup -Identity AdamBertramLovers | 
Set-ADGroup -Description 'More Adam Bertram lovers'

当然,组和用户/计算机之间的关键区别是,组可以包含用户和计算机。当一个计算机或用户账户被包含在一个组中时,我们说它是该组的成员。但是,要添加和更改组成员,你不能使用之前使用的命令。相反,你需要使用Add-ADGroupMemberRemove-ADGroupMember

例如,要将 Jane 添加到我们的组中,可以使用Add-ADGroupMember命令。如果 Jane 想要离开该组,可以使用Remove-ADGroupMember命令将她移除。当你尝试运行这个命令时,你会发现运行Remove-ADGroupMember命令时会弹出一个提示,要求你确认是否要移除该成员:

PS> Get-ADGroup -Identity AdamBertramLovers | Add-ADGroupMember Members 'jjones'
PS> Get-ADGroup -Identity AdamBertramLovers | Remove-ADGroupMember-Members 'jjones'

    Confirm
Are you sure you want to perform this action?
Performing the operation "Set" on target
"CN=AdamBertramLovers,CN=Users,DC=lab,DC=local".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  
[?]
Help (default is "Y"): a

如果你希望跳过此检查,可以添加Force参数,但请注意,得到这个确认提示可能会在某一天拯救你!

项目 5:创建员工入职脚本

让我们把这一切汇总起来,解决另一个实际场景。你的公司雇佣了一名新员工。作为系统管理员,你现在需要执行一系列操作:创建 AD 用户、创建他们的计算机账户,以及将他们添加到特定的组中。你将编写一个脚本来自动化整个过程。

但在你开始这个项目之前——其实在开始任何项目之前——重要的是要弄清楚脚本的功能,并写下一个非正式的定义。对于这个脚本,你需要创建 AD 用户,具体包括:

  • 根据名字和姓氏动态创建用户名

  • 创建并分配一个随机密码给用户

  • 强制用户在登录时更改密码

  • 根据给定的部门设置部门属性

  • 为用户分配一个内部员工编号

接下来,将用户账户添加到一个与部门名称相同的组中。最后,将用户账户添加到一个与员工所在部门名称相同的组织单位中。

现在,明确了这些需求,我们来构建脚本。完成的脚本将命名为New-Employee.ps1,并可以在书籍资源中找到。

你希望这个脚本是可重用的。理想情况下,每当有新员工时,你都可以使用这个脚本。这意味着你需要找到一种智能的方式来处理脚本的输入。通过查看需求,你知道你需要提供一个名字、姓氏、部门和员工编号。列表 11-7 提供了一个脚本大纲,定义了所有参数,并且有一个try/catch块来捕获可能遇到的任何终止错误。#requires语句设置在顶部,以确保每次运行这个脚本时,它都会检查机器上是否已安装ActiveDirectory模块。

#requires -Module ActiveDirectory

[CmdletBinding()]
param (
    [Parameter(Mandatory)]
    [string]$FirstName,

    [Parameter(Mandatory)]
    [string]$LastName,

    [Parameter(Mandatory)]
    [string]$Department,

 [Parameter(Mandatory)]
    [int]$EmployeeNumber
)

try {

} catch {
    Write-Error -Message $_.Exception.Message
}

列表 11-7:基础 New-Employee.ps1 脚本

现在,你已经创建了基础结构,我们来填充try块。

首先,你需要根据我们非正式定义中列出的要求创建一个 AD 用户。你必须动态创建一个用户名。实现这一点的方法有很多:一些组织喜欢用户名是名字的首字母加姓氏,有些喜欢名字和姓氏组合,还有些完全有不同的做法。假设你的公司使用名字首字母加姓氏。如果该用户名已被占用,则会继续从名字中添加下一个字符,直到找到一个唯一的用户名。

我们先处理基本情况。你将对每个字符串对象使用内置的Substring方法来获取名字的首字母。然后,你将姓氏与首字母连接在一起。你将通过字符串格式化来完成这一步,字符串格式化允许你在字符串中定义多个表达式的占位符,并在运行时用值替换这些占位符,示例如下:

$userName = '{0}{1}' -f $FirstName.Substring(0, 1), $LastName

创建初始用户名后,你需要查询 AD,使用Get-ADUser检查该用户名是否已被占用。

Get-ADUser -Filter "samAccountName -eq '$userName'"

如果该命令返回任何内容,则用户名已被占用,你需要尝试下一个用户名。这意味着你需要找到一种方法来动态生成新用户名,并始终为用户名已被占用的情况做好准备。检查不同用户名的一个好方法是使用while循环,条件是你之前对Get-ADUser的调用结果。但你还需要另一个条件来应对如果名字中的字母用完的情况。你不希望循环永远运行下去,所以你会添加另一个条件,$userName –notlike "$FirstName*" ,来停止循环。

while 条件看起来像这样:

(Get-ADUser -Filter "samAccountName -eq '$userName'") –and
($userName -notlike "$FirstName*")

创建了while条件后,你可以完成循环的其余部分:

$i = 2
while ((Get-ADUser -Filter "samAccountName -eq '$userName'") –and
($userName -notlike "$FirstName*")) {
    Write-Warning -Message "The username [$($userName)] already exists. Trying another..."
    $userName = '{0}{1}' -f $FirstName.Substring(0, $i), $LastName
    Start-Sleep -Seconds 1
    $i++
}

对于循环的每次迭代,你通过获取从 0 到i的子字符串,将第一个名字中的一个额外字符添加到建议的用户名中,其中$i是一个计数器变量,它从 2(字符串中的下一个位置)开始,并在每次循环运行时增加。到这个while循环结束时,它要么找到了一个唯一的用户名,要么已经耗尽了所有选项。

如果没有找到现有用户名,你就可以创建你想要的用户名。如果找到了一个用户名,你还需要检查其他事项。你需要检查你将用户账户放入的组织单位(OU)和组是否存在:

if (-not ($ou = Get-ADOrganizationalUnit -Filter "Name –eq '$Department'")) {
    throw "The Active Directory OU for department [$($Department)] could not be found."
} elseif (-not (Get-ADGroup -Filter "Name -eq '$Department'")) {
    throw "The group [$($Department)] does not exist."
}

一旦完成所有检查,你需要创建用户账户。再一次,你需要参考我们的非正式定义:创建并分配一个 随机密码给用户。你希望每次运行这个脚本时生成一个随机密码。生成安全密码的一种简单方法是使用System.Web.Security.Membership对象上的GeneratePassword静态方法,如下所示:

Add-Type -AssemblyName 'System.Web'
$password = [System.Web.Security.Membership]::GeneratePassword(
    (Get-Random Minimum 20 -Maximum 32), 3)
$secPw = ConvertTo-SecureString -String $password -AsPlainText -Force

我选择生成一个至少 20 个字符、最多 32 个字符的密码,但这是完全可以配置的。如果需要,你还可以通过运行Get-ADDefaultDomainPasswordPolicy | Select-object -expand minPasswordLength来查看 AD 的最低密码要求。这个方法甚至允许你指定新密码的长度和复杂度。

现在你已经将密码作为安全字符串获取,你拥有了根据我之前列出的要求创建用户所需的所有参数值。

$newUserParams = @{
    GivenName             = $FirstName
    EmployeeNumber        = $EmployeeNumber
    Surname               = $LastName
    Name                  = $userName
    AccountPassword       = $secPw
    ChangePasswordAtLogon = $true
    Enabled               = $true
    Department            = $Department
    Path                  = $ou.DistinguishedName
    Confirm               = $false
}
New-ADUser @newUserParams

在你创建用户之后,剩下的就是将他们添加到部门组中,你可以使用一个简单的Add-ADGroupMember命令来完成:

Add-ADGroupMember -Identity $Department -Members $userName

一定要查看书中资源中的New-Employee.ps1脚本,以获取此脚本的完整实现版本。

从其他数据源同步

活动目录,特别是在大企业中使用时,可能包含数百万个对象,这些对象每天都被几十个人创建和修改。随着所有这些活动和输入,问题肯定会出现。你将遇到的最大问题之一是保持 AD 数据库与组织的其他部分同步。

公司的 AD 应该与公司组织结构相匹配。这可能意味着每个部门都有自己的 AD 组,每个物理办公室有自己的 OU,等等。无论如何,作为系统管理员,我们的困难任务是确保 AD 始终与组织的其他部分保持同步。这是 PowerShell 的一个重要任务。

使用 PowerShell,你可以将 AD 与几乎任何其他信息源“链接”,这意味着你可以让 PowerShell 不断地读取外部数据源,并根据需要对 AD 进行适当的修改,以创建一个同步过程。

当触发该同步过程时,通常包括以下六个步骤:

  1. 查询外部数据源(SQL 数据库、CSV 文件等)。

  2. 从 AD 中检索对象。

  3. 在源中查找每个对象,AD 具有一个唯一的属性来进行匹配。这个属性通常称为ID。ID 可以是员工 ID,甚至是用户名。唯一重要的是该属性是唯一的。如果找不到匹配项,可以根据源选择性地在 AD 中创建或删除该对象。

  4. 查找一个匹配的单一对象。

  5. 将所有外部数据源映射到 AD 对象属性。

  6. 修改现有的 AD 对象或创建新的对象。

你将在下一节中实施这个计划。

项目 6:创建同步脚本

在本节中,你将学习如何构建一个脚本,将员工信息从 CSV 文件同步到 AD。为此,你需要使用你在第十章中学到的一些命令,以及你在本章之前课程中刚学到的命令。在我们开始之前,建议你浏览一下书中资源中的Employees.csvInvoke-AdCsvSync.ps1,并熟悉项目文件。

构建一个优秀的 AD 同步工具的关键是相似性。我的意思并不是说数据源应该是相同的——因为从技术上讲,它们永远不会相同——而是你需要创建一个脚本,能够以相同的方式查询每个数据存储,并且让每个数据存储返回相同类型的对象。这个难点出现在当你有两个使用不同模式的数据源时。在这种情况下,你可能需要通过将一个字段名映射到另一个字段名来开始做一些转换(正如你将在本章后面所做的那样)。

请考虑以下情况:你已经知道 AD 中每个用户账户都有一些常见的属性——例如名字、姓氏和部门,我们称之为属性模式。然而,可能源数据存储中用于同步的属性永远不会完全相同。即使它们有相同的属性,它们的名称也可能不同。为了解决这个问题,你必须在两个数据存储之间建立映射。

映射数据源属性

创建这种映射的一个简单有效方法是使用哈希表,其中键是第一个数据存储中的属性名称,值是第二个数据存储中的属性名称。为了查看这一过程的实际操作,假设你在一家名为 Acme 的公司工作。Acme 想要将员工记录从 CSV 文件同步到 AD。具体来说,他们想要同步Employees.csv,你可以在本书的资源中找到该文件,或者在这里找到:

"fname","lname","dept"
"Adam","Bertram","IT"
"Barack","Obama","Executive Office"
"Miranda","Bertram","Executive Office"
"Michelle","Obama","Executive Office"

既然你知道 CSV 的表头和 AD 中的属性名称,你可以构建一个映射哈希表,将 CSV 字段的值作为键,AD 属性名称作为值:

$syncFieldMap = @{   
    fname = 'GivenName'
    lname = 'Surname'   
    dept = 'Department'
}

这将处理两个数据存储模式之间的转换。但你还需要为每个员工创建一个唯一的 ID。到目前为止,CSV 的每一行中没有可以匹配到 AD 对象的唯一 ID。例如,你可能会遇到多个名字叫 Adam 的人,多个 IT 部门的员工,或者多个姓 Bertram 的人。这意味着你必须生成自己的唯一 ID。为了简化问题,假设没有两个员工的名字和姓氏相同。否则,创建 ID 的方式可能会依赖于你自己组织的模式。在此假设下,你可以简单地将每个数据存储的名字和姓氏字段连接起来,创建一个临时的唯一 ID。

你将在另一个哈希表中表示这个唯一的 ID。虽然你还没有处理连接操作,但你已经设置好了执行此操作的基础设施:

$fieldMatchIds = @{
    AD = @('givenName','surName')
    CSV = @('fname','lname')
}

现在你已经创建了一种将不同字段映射在一起的方法,可以将该代码整合到几个函数中,以“强制”两个数据存储返回相同的属性,从而实现“苹果对苹果”的比较。

创建返回相似属性的函数

现在你有了哈希表,接下来需要翻译字段名称并构建唯一 ID。你可以创建一个函数来查询我们的 CSV 文件,并输出 AD 理解的属性,以及你可以用来匹配两个数据存储的属性。为此,你将创建一个名为 Get-AcmeEmployeeFromCsv 的函数,代码见列表 11-8。我将 CsvFilePath 参数的值设置为 C:\Employees.csv,假设我们的 CSV 文件位于该位置:

function Get-AcmeEmployeeFromCsv
{    
[CmdletBinding()]
    param (
        [Parameter()]
        [string]$CsvFilePath = 'C:\Employees.csv',
        [Parameter(Mandatory)]
        [hashtable]$SyncFieldMap,
 [Parameter(Mandatory)]
        [hashtable]$FieldMatchIds
    )
    try {
        ## Read each key/value pair in $SyncFieldMap to create calculated
        ## fields which we can pass to Select-Object later. This allows us to
        ## return property names that match Active Directory attributes rather
        ## than what's in the CSV file.
     ❶ $properties = $SyncFieldMap.GetEnumerator() | ForEach-Object {
            @{
                Name = $_.Value
                Expression = [scriptblock]::Create("`$_.$($_.Key)")
            }
        }
        ## Create the unique ID based on the unique fields defined in
        ## $FieldMatchIds
     ❷ $uniqueIdProperty = '"{0}{1}" -f '
        $uniqueIdProperty = $uniqueIdProperty += 
        ($FieldMatchIds.CSV | ForEach-Object { '$_.{0}' -f $_ }) – join ','
        $properties += @{
            Name = 'UniqueID'
            Expression = [scriptblock]::Create($uniqueIdProperty)
        }
        ## Read the CSV file and "transform" the CSV fields to AD attributes
        ## so we can compare apples to apples
     ❸ Import-Csv -Path $CsvFilePath | Select-Object – Property $properties
    } catch {
        Write-Error -Message $_.Exception.Message
    }
}

列表 11-8:Get-AcmeEmployeeFromCsv 函数

该函数的工作流程分为三个主要步骤:首先,将 CSV 的属性映射到 AD 属性 ❶;接着,创建一个唯一 ID 并将其作为属性 ❷;最后,读取 CSV,并使用 Select-Object 和计算属性返回你需要的属性 ❸。

如下代码所示,你可以将 $syncFieldMap 哈希表和 $fieldMatchIds 哈希表传递给你新的 Get-AcmeEmployeeFromCsv 函数,你可以用它来返回与 Active Directory 属性以及你新创建的唯一 ID 同步的属性名称:

PS> Get-AcmeEmployeeFromCsv -SyncFieldMap $syncFieldMap 
-FieldMatchIds $fieldMatchIds

GivenName Department       Surname UniqueID
--------- ----------       ------- --------
Adam      IT               Bertram AdamBertram
Barack    Executive Office Obama   BarackObama
Miranda   Executive Office Bertram MirandaBertram
Michelle  Executive Office Obama   MichelleObama

现在,你需要构建一个从 AD 查询的函数。幸运的是,这一次你不需要转换任何属性名称,因为 AD 的属性名称就是你的公共集合。在这个函数中,你只需要调用 Get-ADUser,并确保返回你需要的属性,代码见列表 11-9。

function Get-AcmeEmployeeFromAD
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [hashtable]$SyncFieldMap,

        [Parameter(Mandatory)]
        [hashtable]$FieldMatchIds
    )

    try {
        $uniqueIdProperty = '"{0}{1}" -f '
        $uniqueIdProperty += ($FieldMatchIds.AD | ForEach Object { '$_.{0}' -f $_ }) -join ','

        $uniqueIdProperty = @{ ❶
            Name = 'UniqueID'
            Expression = [scriptblock]::Create($uniqueIdProperty)
        }

        Get-ADUser -Filter * -Properties @($SyncFieldMap.Values) | Select-Object *,$uniqueIdProperty ❷

    } catch {
        Write-Error -Message $_.Exception.Message
    }
}

列表 11-9:Get-AcmeEmployeeFromAD 函数

再次,我将重点介绍这段代码的主要步骤:首先,创建唯一 ID 来执行匹配 ❶;然后,查询 AD 用户并仅返回字段映射哈希表中的值,同时返回你之前创建的唯一 ID ❷。

当你运行这段代码时,你会看到它返回具有适当属性和唯一 ID 属性的 AD 用户帐户。

在 Active Directory 中查找匹配项

现在你有了两个类似的函数,可以从数据存储中提取信息,并返回相同的属性名称。接下来的步骤是查找 CSV 和 AD 之间的所有匹配项。为了简化这个过程,你将使用列表 11-10 中的代码,创建另一个名为 Find-UserMatch 的函数,该函数将执行这两个函数,并收集这两个数据集。一旦获取了数据,它将查找 UniqueID 字段上的匹配项。

function Find-UserMatch {
    [OutputType()]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [hashtable]$SyncFieldMap,

        [Parameter(Mandatory)]
        [hashtable]$FieldMatchIds 
    )
    $adusers = Get-AcmeEmployeeFromAD -SyncFieldMap $SyncFieldMap -FieldMatchIds $FieldMatchIds ❶

    $csvUsers = Get-AcmeEmployeeFromCSV -SyncFieldMap $SyncFieldMap -FieldMatchIds $FieldMatchIds ❷

    $adUsers.foreach({
        $adUniqueId = $_.UniqueID
        if ($adUniqueId) { ❸
            $output = @{
                CSVProperties = 'NoMatch'
                ADSamAccountName = $_.samAccountName
            }
            if ($adUniqueId -in $csvUsers.UniqueId) { ❹
                $output.CSVProperties = ($csvUsers.Where({$_.UniqueId -eq $adUniqueId})) ❺
            }
            [pscustomobject]$output
        }
    })
}

列表 11-10:查找用户匹配项

让我们逐步分析这段代码。首先,从 AD 获取用户列表 ❶;然后,从我们的 CSV 获取用户列表 ❷。对于每个来自 AD 的用户,检查 UniqueID 属性是否已被填充 ❸。如果已填充,检查 CSV 和 AD 用户之间是否找到了匹配 ❹,如果找到了,在我们的自定义对象中创建一个名为 CSVProperties 的属性,包含与匹配用户相关的所有属性 ❺。

如果找到匹配项,函数将返回 AD 用户的 samAccountName 和所有 CSV 属性;否则,它将返回 NoMatch。返回 samAccountName 会给你一个 AD 中的唯一 ID,这样你以后就可以查找这个用户。

PS> Find-UserMatch -SyncFieldMap $syncFieldMap -FieldMatchIds $fieldMatchIds

ADSamAccountName CSVProperties
---------------- -------------
user             NoMatch
abertram         {@{GivenName=Adam; Department=IT;
                 Surname=Bertram; UniqueID=AdamBertram}}
dbddar           NoMatch
jjones           NoMatch
BSmith           NoMatch

到目前为止,你已经有了一个功能,可以在 AD 数据和 CSV 数据之间找到 1:1 匹配。你现在准备好开始进行大量的 AD 更改了,这虽然令人兴奋,但也有些吓人!

更改 Active Directory 属性

现在你有了一种方法,可以找出哪个 CSV 行对应哪个 AD 用户帐户。你可以使用 Find-UserMatch 函数通过用户的唯一 ID 查找 AD 用户,然后更新其 AD 信息,使其与 CSV 中的数据匹配,如 清单 11-11 所示。

## Find all of the CSV <--> AD user account matches
$positiveMatches = (Find-UserMatch -SyncFieldMap $syncFieldMap -FieldMatchIds $fieldMatchIds).where({ $_.CSVProperties -ne 'NoMatch' })
foreach ($positiveMatch in $positiveMatches) {
    ## Create the splatting parameters for Set-ADUser using
    ## the identity of the AD samAccountName
    $setADUserParams = @{
        Identity = $positiveMatch.ADSamAccountName
    }

    ## Read each property value that was in the CSV file
    $positiveMatch.CSVProperties.foreach({
        ## Add a parameter to Set-ADUser for all of the CSV
        ## properties excluding UniqueId
        ## Find all of the properties on the CSV row that are NOT UniqueId
        $_.PSObject.Properties.where({ $_.Name –ne 'UniqueID' }).foreach({
            $setADUserParams[$_.Name] = $_.Value
        })
    })
    Set-ADUser @setADUserParams
}

清单 11-11:将 CSV 同步到 AD 属性

创建一个健壮且灵活的 AD 同步脚本需要做很多工作。在这个过程中,你会遇到很多小细节和问题,尤其是当你构建更复杂的脚本时。

我们才刚刚触及与 PowerShell 同步的表面。如果你想看看你能通过这个概念做多少事情,可以查看 PowerShell Gallery 中的 PSADSync 模块(Find-Module PSADSync)。这个模块是专为我们这里的任务而构建的,但它可以处理更复杂的情况。如果在这个练习中你感觉有些迷茫,我强烈建议你重新阅读代码——多少遍都没关系。学习 PowerShell 的唯一真正方法就是实验!运行代码,看看它出错,自己修复,再试一次。

小结

在本章中,你熟悉了 ActiveDirectory PowerShell 模块。你学习了如何在 AD 中创建和更新用户、计算机和组。通过几个实际的例子,你看到了如何使用 PowerShell 自动化繁琐的 Active Directory 工作。

在接下来的两章中,我们将进入云端!我们将继续自动化所有任务,并看看如何在 Microsoft Azure 和 Amazon Web Services(AWS)中自动化一些常见任务。

第十二章:与 Azure 一起工作

Images

随着组织将越来越多的服务迁移到云端,自动化人员了解如何在云端工作变得至关重要。幸运的是,借助 PowerShell 的模块以及它与几乎任何 API 兼容的能力,在云端工作变得轻松无比。在本章和下一章中,我将向你展示如何使用 PowerShell 自动化任务;在本章,你将与 Microsoft Azure 一起工作,下一章则是与 Amazon Web Services 一起工作。

前提条件

如果你将在本章中运行代码,我对你的环境做了几点假设。第一个是假设你已经设置了 Microsoft Azure 订阅。在本章中,你将使用真实的云资源,所以你的账户会产生费用,但费用应该是合理的。只要你不让任何虚拟机长时间运行,费用应该不会超过 10 美元。

一旦你设置了 Azure 订阅,你需要 Az PowerShell 模块包。微软提供的这个模块包包含数百个命令,用于执行几乎所有 Azure 服务的任务。你可以通过在控制台中运行 Install-Module Az 来下载它(确保以管理员身份运行)。我应该指出,我正在使用 Az 模块的 2.4.0 版本。如果你使用的是更新的版本,我不能保证所有这些命令会完全相同地工作。

Azure 身份验证

Azure 提供了几种身份验证方式。在本章中,你将使用服务主体。服务主体是一个 Azure 应用程序的身份。它是代表一个应用程序的对象,可以为该应用程序分配各种权限。

为什么要创建一个服务主体?你希望通过使用不需要用户交互的自动化脚本来进行 Azure 身份验证。为此,Azure 要求你使用服务主体或组织帐户。我希望每个人都能跟上,不论他们使用哪种类型的帐户,所以你将使用服务主体来进行 Azure 身份验证。

创建服务主体

与直觉相反,创建服务主体的第一步是采用传统方式进行身份验证。为此,使用 Connect-AzAccount,它会弹出一个类似于图 12-1 的窗口。

Image

图 12-1:Connect-AzAccount 凭证提示

提供你的 Azure 用户名和密码后,窗口应该关闭,并显示类似于清单 12-1 的输出。

PS> Connect-AzAccount

Environment           : AzureCloud
Account               : email
TenantId              : tenant id
SubscriptionId        : subscription id
SubscriptionName      : subscription name
CurrentStorageAccount :

清单 12-1:Connect-AzAccount 输出

请确保记录下订阅 ID 和租户 ID。稍后在脚本中你将需要这些信息。如果由于某些原因,在这里通过 Connect-AzAccount 进行身份验证时没有捕获到它们,你可以稍后使用 Get-AzSubscription 命令获取它们。

现在你已经完成了(交互式)认证,可以开始创建服务主体了。这个过程分为三个步骤:首先,创建一个新的 Azure AD 应用;然后,创建服务主体本身;最后,为该服务主体创建角色分配。

你可以使用任何你喜欢的名称和 URI 来创建 Azure AD 应用(清单 12-2)。对于我们的目的,使用什么 URI 并不重要,但创建 AD 应用时必须提供 URI。为了确保你有足够的权限来创建 AD 应用,参见https://docs**.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals.

PS> ❶$secPassword = ConvertTo-SecureString -AsPlainText -Force -String 'password'
PS> ❷$myApp = New-AzADApplication -DisplayName AppForServicePrincipal -IdentifierUris
'http://Some URL here' -Password $secPassword

清单 12-2:创建 Azure AD 应用

你可以看到,你首先通过使用密码❶来创建一个安全字符串。将密码转换为正确格式后,你会创建一个新的 Azure AD 应用❷。服务主体需要创建一个 Azure AD 应用。

接下来,你使用New-AzADServicePrincipal命令来创建服务主体,如清单 12-3 所示。你引用了清单 12-2 中创建的应用。

PS> $sp = New-AzADServicePrincipal -ApplicationId $myApp.ApplicationId
PS> $sp

ServicePrincipalNames : {application id, http://appforserviceprincipal}
ApplicationId         : application id
DisplayName           : AppForServicePrincipal
Id                    : service principal id
Type                  : ServicePrincipal

清单 12-3:使用 PowerShell 创建 Azure 服务主体

最后,你需要为服务主体分配一个角色。清单 12-4 为服务主体分配了一个Contributor角色,以确保该服务主体具备执行本章所有任务所需的访问权限。

PS> New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName 
$sp.ServicePrincipalNames[0]

RoleAssignmentId   : /subscriptions/subscription id/providers/Microsoft.Authorization/
                     roleAssignments/assignment id
Scope              : /subscriptions/subscription id
DisplayName        : AppForServicePrincipal
SignInName         :
RoleDefinitionName : Contributor
RoleDefinitionId   : id
ObjectId           : id
ObjectType         : ServicePrincipal
CanDelegate        : False

清单 12-4:为服务主体创建角色分配

这样,服务主体就创建完成并分配了角色。

剩下的唯一任务是将表示为安全字符串的加密密码保存到磁盘上的某个位置。你可以使用ConvertFrom-SecureString命令来实现。ConvertFrom-SecureString命令(ConvertTo-SecureString的互补命令)将表示为 PowerShell 安全字符串的加密文本转换为普通字符串,从而允许你稍后保存并引用它:

PS> $secPassword | ConvertFrom-SecureString | Out-File -FilePath C:\AzureAppPassword.txt

一旦你将密码保存到磁盘上,你就可以开始为 Azure 设置非交互式认证了。

使用 Connect-AzAccount 进行非交互式认证

Connect-AzAccount命令会提示你手动输入用户名和密码。在你的脚本中,你希望尽可能减少交互操作,因为你最不希望依赖某个人坐在电脑前输入密码!幸运的是,你还可以将PSCredential对象传递给Connect-AzAccount

你将编写一个小脚本来处理非交互式认证。首先,让我们创建一个包含 Azure 应用 ID 和密码的PSCredential对象:

$azureAppId = 'application id'
$azureAppIdPasswordFilePath = 'C:\AzureAppPassword.txt'
$pwd = (Get-Content -Path $azureAppIdPasswordFilePath | ConvertTo-SecureString)
$azureAppCred = (New-Object System.Management.Automation.PSCredential $azureAppId,$pwd)

还记得你之前写下的订阅 ID 和租户 ID 吗?你也需要将它们传递给Connect-AzAccount

$subscriptionId = 'subscription id'
$tenantId = 'tenant id'
Connect-AzAccount -ServicePrincipal -SubscriptionId $subscriptionId -TenantId $tenantId
-Credential $azureAppCred

你已经为非交互式认证做好了准备!现在你完成了这个设置,它将被保存下来,以便以后无需再次进行认证。

如果你想要简化的代码,可以从本章的书籍资源中下载AzureAuthentication.ps1脚本。

创建 Azure 虚拟机及其所有依赖项

现在,是时候设置一个 Azure 虚拟机了。Azure 虚拟机是 Azure 最受欢迎的服务之一,掌握构建 Azure 虚拟机的技能对任何在 Azure 环境中工作的人来说都是一项重大优势。

很久以前,当我第一次创建我的 Azure 订阅并想玩虚拟机时,我以为会有一个单独的命令来设置它——就像我只需要运行New-AzureVm,然后 voilà!就会有一台全新的虚拟机让我玩。哦,我错得可真大。

我没意识到在虚拟机真正能够工作之前,有这么多的依赖关系需要设置。你有没有注意到本章的前提部分有多短?我故意这么做的,原因是:为了获得更多使用 PowerShell 的经验,你需要安装所有创建 Azure 虚拟机所需的依赖项。你将安装资源组、虚拟网络、存储账户、公共 IP 地址、网络接口和操作系统镜像。换句话说,你将从头开始构建这台虚拟机。我们开始吧!

创建资源组

在 Azure 中,所有东西都是资源,而且所有东西都必须放在资源组内。你首先要做的就是创建一个资源组。为此,你将使用New-AzResourceGroup命令。此命令需要资源组名称以及它将被创建的地理区域。在这个例子中,你将创建一个名为PowerShellForSysAdmins-RG的资源组,并将其放置在美国东部区域(如 Listing 12-5 所示)。你可以通过运行Get-AzLocation命令来查找所有可用的区域。

PS> New-AzResourceGroup -Name 'PowerShellForSysAdmins-RG' -Location 'East US'

Listing 12-5:创建 Azure 资源组

一旦资源组创建完成,就可以开始构建虚拟机将使用的网络堆栈。

创建网络堆栈

为了让你的虚拟机连接到外部世界和其他 Azure 资源,它需要一个网络堆栈:子网、虚拟网络、公共 IP 地址(可选)和虚拟网络适配器(vNIC)。

子网

你的第一步是创建一个子网。子网是一个逻辑网络,IP 地址可以相互通信,而不需要路由器。子网将“进入”虚拟网络。子网将虚拟网络划分为更小的网络。

要创建子网配置,请使用New-AzVirtualNetworkSubnetConfig命令(见 Listing 12-6)。此命令需要一个名称和 IP 地址前缀或网络标识。

PS> $newSubnetParams = @{
 'Name' = 'PowerShellForSysAdmins-Subnet'
 'AddressPrefix' = '10.0.1.0/24'
}
PS> $subnet = New-AzVirtualNetworkSubnetConfig @newSubnetParams

Listing 12-6:创建虚拟网络子网配置

您为子网指定了 PowerShellForSysAdmins-Subnet 的名称,并使用前缀 10.0.1.0/24。

虚拟网络

现在,您已经创建了一个子网配置,可以使用它来创建虚拟网络。虚拟网络是一个 Azure 资源,它允许您将虚拟机等资源与所有其他资源进行隔离。虚拟网络可以理解为您在本地网络路由器中可能实现的逻辑网络。

要创建虚拟网络,请使用 New-AzVirtualNetwork 命令,如列表 12-7 所示。

PS> $newVNetParams = @{
 ❶ 'Name' = 'PowerShellForSysAdmins-vNet'
 ❷ 'ResourceGroupName' = 'PowerShellForSysAdmins-RG'
 ❸ 'Location' = 'East US'
 ❹ 'AddressPrefix' = '10.0.0.0/16'
}
PS> $vNet = New-AzVirtualNetwork @newVNetParams -Subnet $subnet

列表 12-7:创建虚拟网络

请注意,要创建虚拟网络,您需要指定网络名称 ❶、资源组 ❷、区域(位置) ❸ 和子网所属的整体私有网络 ❹。

公共 IP 地址

现在,您已经设置了一个虚拟网络,您需要一个公共 IP 地址,以便将虚拟机连接到互联网,并允许客户端连接到您的虚拟机。请注意,如果您计划将虚拟机仅提供给其他 Azure 资源,那么这一过程在技术上并不是必须的。但由于您对虚拟机有更大的规划,因此您将继续执行此步骤。

同样,您可以通过使用一条命令来创建公共 IP 地址:New-AzPublicIpAddress。您之前已经见过这个功能的大部分参数,但请注意有一个新的参数,名为 AllocationMethod。这个参数告诉 Azure 是否创建动态或静态 IP 地址资源。如列表 12-8 所示,指定您需要一个动态 IP 地址。您为虚拟机分配动态 IP 地址,因为这可以减少一个需要担心的任务。由于您不要求 IP 地址始终保持相同,使用动态 IP 地址可以让您免去另一项麻烦。

PS> $newPublicIpParams = @{
 'Name' = 'PowerShellForSysAdmins-PubIp'
 'ResourceGroupName' = 'PowerShellForSysAdmins-RG'
 'AllocationMethod' = 'Dynamic' ## Dynamic or Static
 'Location' = 'East US'
}
PS> $publicIp = New-AzPublicIpAddress @newPublicIpParams

列表 12-8:创建公共 IP 地址

尽管这个公共 IP 地址已经存在,但它没有任何作用,因为它尚未与任何内容关联。您需要将其绑定到 vNIC。

虚拟网络适配器

要构建 vNIC,您需要执行另一个单行命令 New-AzNetworkInterface,并且可以使用您之前使用的大部分相同参数。您还需要先前创建的子网和公共 IP 地址的 ID。子网和公共 IP 地址都存储为具有 ID 属性的对象;您只需访问该属性,如列表 12-9 所示。

PS> $newVNicParams = @{
 'Name' = 'PowerShellForSysAdmins-vNIC'
 'ResourceGroupName' = 'PowerShellForSysAdmins-RG'
 'Location' = 'East US'
 'SubnetId' = $vNet.Subnets[0].Id
 'PublicIpAddressId' = $publicIp.Id
}
PS> $vNic = New-AzNetworkInterface @newVNicParams

列表 12-9:创建 Azure vNIC

您的网络堆栈已经完成!下一步是创建存储帐户。

创建存储帐户

你需要为虚拟机指定存储位置。这个位置叫做 存储帐户。创建一个基本存储帐户和使用 New-AzStorageAccount 命令一样简单。与前面几条命令一样,你需要一个名称、资源组和位置;但这里有一个新的 Type 参数,它指定你的存储帐户的冗余级别。使用最便宜的存储帐户类型(本地冗余),通过 Standard_LRS 参数指定,如清单 12-10 所示。

PS> $newStorageAcctParams = @{
 'Name' = 'powershellforsysadmins'
 'ResourceGroupName' = 'PowerShellForSysAdmins-RG'
 'Type' = 'Standard_LRS'
 'Location' = 'East US'
}
PS> $storageAccount = New-AzStorageAccount @newStorageAcctParams

清单 12-10:创建 Azure 存储帐户

现在你有了虚拟机的存储位置,是时候设置操作系统映像了。

创建操作系统映像

操作系统映像 是虚拟机将使用的虚拟磁盘的基础。你不会在虚拟机上安装 Windows,而是使用一个现有的操作系统映像,将你带到只需启动它的阶段。

创建操作系统映像分为两步:定义一些操作系统配置设置,然后定义要使用的 offer 或操作系统映像。Azure 使用 offer 这个术语来引用虚拟机映像。

为了设置所有配置选项,你需要构建一个虚拟机配置对象。此对象定义了你正在创建的虚拟机的名称和大小。你可以使用 New-AzVMConfig 命令来完成。在清单 12-11 中,你创建了一个 Standard_A3 虚拟机。(你可以运行 Get-AzVMSize 并指定区域,来查找所有可用的虚拟机大小。)

PS> $newConfigParams = @{
 'VMName' = 'PowerShellForSysAdmins-VM'
 'VMSize' = 'Standard_A3'
}
PS> $vmConfig = New-AzVMConfig @newConfigParams

清单 12-11:创建虚拟机配置

配置创建完成后,你可以将该对象作为 VM 参数传递给 Set-AzVMOperatingSystem 命令。此命令允许你定义操作系统特定的属性,例如虚拟机的主机名,并启用 Windows 更新及其他属性。我们在这里保持简单,但如果你想查看所有可能的选项,可以使用 Get-Help 查看 Set-AzVMOperatingSystem 的详细信息。

清单 12-12 创建了一个 Windows 操作系统对象,该对象的主机名为 Automate-VM(注意:主机名必须少于 16 个字符)。你将使用 Get-Credential 命令返回的用户名和密码来创建一个新的管理员用户,并使用提供的密码;同时,使用 EnableAutoUpdate 参数自动应用任何新的 Windows 更新。

PS> $newVmOsParams = @{
 'Windows' = $true
 'ComputerName' = 'Automate-VM'
 'Credential' = (Get-Credential -Message 'Type the name and password of the
    local administrator account.')
 'EnableAutoUpdate' = $true
 'VM' = $vmConfig
}
PS> $vm = Set-AzVMOperatingSystem @newVmOsParams

清单 12-12:创建操作系统映像

现在你需要创建一个 VM offer。offer 是 Azure 允许你选择安装在虚拟机操作系统磁盘上的操作系统类型的方式。此示例使用的是 Windows Server 2012 R2 数据中心映像。这是微软提供的映像,因此无需创建自己的映像。

一旦你创建了 offer 对象,你可以通过使用 Set-AzVMSourceImage 命令来创建源映像,如清单 12-13 所示。

PS> $offer = Get-AzVMImageOffer -Location 'East US'❶ –PublisherName
'MicrosoftWindowsServer'❷ | Where-Object { $_.Offer -eq 'WindowsServer' }❸
PS> $newSourceImageParams = @{
 'PublisherName' = 'MicrosoftWindowsServer'
 'Version' = 'latest'
 'Skus' = '2012-R2-Datacenter'
 'VM' = $vm
 'Offer' = $offer.Offer
}
PS> $vm = Set-AzVMSourceImage @newSourceImageParams

清单 12-13:查找和创建虚拟机源映像

在这里,你正在查询东部美国区域❶中所有发布商名称为MicrosoftWindowsServer ❷的可用项。你可以使用Get-AzVMImagePublisher来查找发布商列表。然后,你将可用项限制为名为WindowsServer ❸的项。分配了源镜像后,你现在可以将该镜像分配给虚拟机对象。这样就完成了虚拟机虚拟磁盘的设置。

要将镜像分配给虚拟机对象,你需要一个你刚刚创建的操作系统磁盘的 URI,并且需要将该 URI 与虚拟机对象一起传递给Set-AzVMOSDisk命令(清单 12-14)。

PS> $osDiskName = 'PowerShellForSysAdmins-Disk'
PS> $osDiskUri = '{0}vhds/PowerShellForSysAdmins-VM{1}.vhd' -f $storageAccount
                 .PrimaryEndpoints.Blob.ToString(), $osDiskName
PS> $vm = Set-AzVMOSDisk -Name $osDiskName -CreateOption 'fromImage' -VM $vm -VhdUri $osDiskUri

清单 12-14:将操作系统磁盘分配给虚拟机

到此为止,你已经有了操作系统磁盘,并且它已经分配给了虚拟机对象。现在是时候完成这项工作了!

总结

几乎完成了。剩下的就是附加你之前创建的 vNIC,并且,嗯,创建实际的虚拟机。

要将 vNIC 附加到虚拟机,你需要使用Add-AzVmNetworkInterface命令,并传递你创建的虚拟机对象以及之前创建的 vNIC 的 ID—你可以在清单 12-15 中查看所有这些。

PS> $vm = Add-AzVMNetworkInterface -VM $vm -Id $vNic.Id

清单 12-15:将 vNIC 附加到虚拟机

现在,终于可以创建虚拟机,如清单 12-16 所示。通过调用New-AzVm命令并提供虚拟机对象、资源组和区域,你就拥有了自己的虚拟机!请注意,这会启动虚拟机,并且此时你将开始产生费用。

PS> New-AzVM -VM $vm -ResourceGroupName 'PowerShellForSysAdmins-RG' -Location 'East US'

RequestId IsSuccessStatusCode StatusCode ReasonPhrase
--------- ------------------- ---------- ------------
                         True         OK OK

清单 12-16:创建 Azure 虚拟机

你应该已经在 Azure 中拥有一个全新的虚拟机,名为Automate-VM。为了确认,你可以运行Get-AzVm来确保虚拟机存在。查看清单 12-17 中的输出。

PS> Get-AzVm -ResourceGroupName 'PowerShellForSysAdmins-RG' -Name PowerShellForSysAdmins-VM

ResourceGroupName  : PowerShellForSysAdmins-RG
Id                 : /subscriptions/XXXXXXXXXXXXX/resourceGroups/PowerShellForSysAdmins-RG/
                     providers/Microsoft.Compute/virtualMachines/PowerShellForSysAdmins-VM
VmId               : e459fb9e-e3b2-4371-9bdd-42ecc209bc01
Name               : PowerShellForSysAdmins-VM
Type               : Microsoft.Compute/virtualMachines
Location           : eastus
Tags               : {}
DiagnosticsProfile : {BootDiagnostics}
Extensions         : {BGInfo}
HardwareProfile    : {VmSize}
NetworkProfile     : {NetworkInterfaces}
OSProfile          : {ComputerName, AdminUsername, WindowsConfiguration, Secrets}
ProvisioningState  : Succeeded
StorageProfile     : {ImageReference, OsDisk, DataDisks}

清单 12-17:发现你的 Azure 虚拟机

如果你看到类似的输出,那么你已经成功创建了一个 Azure 虚拟机!

自动化虚拟机创建

呼!创建一个虚拟机并建立所有依赖关系真是件大事;当我想要创建下一个虚拟机时,我真不希望再经历一遍。为什么我们不创建一个函数来为我们处理这一切呢?通过使用函数,我们可以将刚才的所有代码整合到一个单独的、可执行的代码块中,并且可以重复使用。

如果你有冒险精神,我创建了一个名为New-CustomAzVm的自定义 PowerShell 函数,可以在本章的资源中找到。它提供了一个很好的示例,展示了如何将本节中完成的所有任务整合成一个简洁的函数,并且只需要最少的输入。

部署 Azure Web 应用

如果你在使用 Azure,你可能想了解如何部署 Azure web 应用。Azure web 应用 允许你快速配置网站和其他各种 web 服务,这些服务运行在 IIS、Apache 等服务器上,而无需担心构建 web 服务器本身。一旦你学会如何使用 PowerShell 部署 Azure web 应用,你就可以将这个过程融入到更大的工作流中,包括开发构建管道、测试环境配置、实验室配置等。

部署 Azure web 应用是一个两步过程:首先创建应用服务计划,然后创建 web 应用本身。Azure web 应用是 Azure 应用服务的一部分,任何属于此类别的资源都必须关联一个应用服务计划。应用服务计划 告诉 web 应用要构建程序的底层计算资源类型。

创建应用服务计划和 web 应用

创建 Azure 服务计划相当简单。和之前一样,你只需要一个命令。此命令要求你提供应用服务计划的名称、所在的区域或位置、资源组,以及一个可选的层级,定义托管 web 应用的服务器的性能类型。

就像在前一节中做的一样,你需要创建一个资源组,将所有资源放在一起;使用以下命令:New-AzResourceGroup -Name 'PowerShellForSysAdmins-App' -Location 'East US'。一旦资源组创建完成,你就可以创建应用服务计划,并将其放入该资源组中。

你的 web 应用名为 Automate,将位于 East US 区域,并且属于 Free 层级的应用。你可以在 清单 12-18 中看到完成这些任务的所有代码。

PS> New-AzAppServicePlan -Name 'Automate' -Location 'East US'
-ResourceGroupName 'PowerShellForSysAdmins-App' -Tier 'Free'

清单 12-18:创建 Azure 应用服务计划

一旦执行了此命令,你将创建一个应用服务计划,并可以继续创建 web 应用本身。

你可能不会惊讶地发现,使用 PowerShell 创建 Azure web 应用也是一个单命令过程。只需运行 New-AzWebApp,并提供现在常见的参数:资源组名称、应用名称和位置,以及该 web 应用所在的应用服务计划。

清单 12-19 使用 New-AzWebApp 命令创建一个名为 MyApp 的 web 应用,位于 PowerShellForSysAdmins-App 资源组内,使用之前创建的应用服务计划 Automate。请注意,这会启动应用程序,可能会产生费用。

PS> New-AzWebApp -ResourceGroupName 'PowerShellForSysAdmins-App' -Name
'AutomateApp' -Location 'East US' -AppServicePlan 'Automate'

清单 12-19:创建 Azure web 应用

当你运行此命令时,你应该在输出中看到许多属性;这些是 web 应用的各种设置。

部署 Azure SQL 数据库

另一个常见的 Azure 任务是部署 Azure SQL 数据库。要部署 Azure SQL 数据库,你需要做三件事:创建 Azure SQL 服务器(数据库将运行在其上),创建数据库本身,然后创建一个 SQL Server 防火墙规则来连接到数据库。

如前所述,你需要创建一个资源组来容纳所有新的资源。可以运行New-AzResourceGroup -Name 'PowerShellForSysAdmins-SQL' -Location 'East US'来创建资源组。然后,你将创建一个 SQL 服务器来运行数据库。

创建 Azure SQL 服务器

创建 Azure SQL 服务器需要另一个单行命令:New-AzSqlServer。同样,你需要提供资源名称、服务器名称以及区域,但在这里,你还需要提供 SQL 管理员用户的用户名和密码。这需要更多的工作。由于你需要创建一个凭证传递给New-AzSqlServer,我们先创建该凭证。我已经在“创建服务主体”一节中讲解了如何创建一个PSCredential对象,具体内容见第 158 页,所以这里不再赘述。

PS> $userName = 'sqladmin'
PS> $plainTextPassword = 's3cretp@SSw0rd!'
PS> $secPassword = ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force
PS> $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList
$userName,$secPassword

一旦你有了凭证,其余的工作就像将所有参数放入哈希表,并传递给New-AzSqlServer函数一样简单,具体内容见清单 12-20。

PS> $parameters = @{
 ResourceGroupName = 'PowerShellForSysAdmins-SQL'
 ServerName = 'powershellforsysadmins-sqlsrv'
 Location =  'East US'
 SqlAdministratorCredentials = $credential
}
PS> New-AzSqlServer @parameters

ResourceGroupName        : PowerShellForSysAdmins-SQL
ServerName               : powershellsysadmins-sqlsrv
Location                 : eastus
SqlAdministratorLogin    : sqladmin
SqlAdministratorPassword :
ServerVersion            : 12.0
Tags                     :
Identity                 :
FullyQualifiedDomainName : powershellsysadmins-sqlsrv.database.windows.net
ResourceId               : /subscriptions/XXXXXXXXXXXXX/resourceGroups
                           /PowerShellForSysAdmins-SQL/providers/Microsoft.Sql
                           /servers/powershellsysadmins-sqlsrv

清单 12-20:创建 Azure SQL 服务器

现在 SQL 服务器已经创建,你已经为数据库奠定了基础。

创建 Azure SQL 数据库

要创建 SQL 数据库,请使用New-AzSqlDatabase命令,具体内容见清单 12-21。除了常见的ResourceGroupName参数外,还需传入你刚才创建的服务器名称以及你要创建的数据库名称(在此示例中为AutomateSQLDb)。

PS> New-AzSqlDatabase -ResourceGroupName 'PowerShellForSysAdmins-SQL'
-ServerName 'PowerShellSysAdmins-SQLSrv' -DatabaseName 'AutomateSQLDb'

ResourceGroupName             : PowerShellForSysAdmins-SQL
ServerName                    : PowerShellSysAdmins-SQLSrv
DatabaseName                  : AutomateSQLDb
Location                      : eastus
DatabaseId                    : 79f3b331-7200-499f-9fba-b09e8c424354
Edition                       : Standard
CollationName                 : SQL_Latin1_General_CP1_CI_AS
CatalogCollation              :
MaxSizeBytes                  : 268435456000
Status                        : Online
CreationDate                  : 9/15/2019 6:48:32 PM
CurrentServiceObjectiveId     : 00000000-0000-0000-0000-000000000000
CurrentServiceObjectiveName   : S0
RequestedServiceObjectiveName : S0
RequestedServiceObjectiveId   :
ElasticPoolName               :
EarliestRestoreDate           : 9/15/2019 7:18:32 PM
Tags                          :
ResourceId                    : /subscriptions/XXXXXXX/resourceGroups
                                /PowerShellForSysAdmins-SQL/providers
                                /Microsoft.Sql/servers/powershellsysadmin-sqlsrv
                                /databases/AutomateSQLDb
CreateMode                    :
ReadScale                     : Disabled
ZoneRedundant                 : False
Capacity                      : 10
Family                        :
SkuName                       : Standard
LicenseType                   :

清单 12-21:创建 Azure SQL 数据库

到此为止,你已经在 Azure 中运行了一个 SQL 数据库。但当你尝试连接时,它无法正常工作。默认情况下,创建新的 Azure SQL 数据库时,它会锁定所有外部连接。你需要创建一个防火墙规则,允许连接到你的数据库。

创建 SQL 服务器防火墙规则

创建防火墙规则的命令是New-AzSqlServerFirewallRule。此命令需要传入资源组名称、你之前创建的服务器名称、防火墙规则名称以及起始和结束的 IP 地址。起始和结束的 IP 地址可以让你指定一个单一的 IP 地址或 IP 地址范围,允许其连接到你的数据库。由于你只会在一台本地计算机上管理 Azure,我们将把 SQL 服务器的连接限制为仅来自当前计算机。为此,你需要先找出你的公共 IP 地址。你可以通过一个 PowerShell 单行命令轻松获取:Invoke-RestMethod http://ipinfo.io/json | Select -ExpandProperty ip。然后,你可以将公共 IP 地址用作StartIPAddressEndIPAddress参数。然而,请注意,如果你的公共 IP 地址发生变化,你需要重新执行这些操作。

同时,请注意,清单 12-22 中的服务器名称必须由全小写字母、连字符和/或数字组成。否则,当你尝试创建防火墙规则时,会出现错误。

PS> $parameters = @{
 ResourceGroupName = 'PowerShellForSysAdmins-SQL'
 FirewallRuleName = 'PowerShellForSysAdmins-FwRule'
 ServerName = 'powershellsysadmin-sqlsrv'
 StartIpAddress = 'Your Public IP Address'
 EndIpAddress = 'Your Public IP Address'
}
PS> New-AzSqlServerFirewallRule @parameters

ResourceGroupName : PowerShellForSysAdmins-SQL
ServerName        : powershellsys-sqlsrv
StartIpAddress    : 0.0.0.0
EndIpAddress      : 0.0.0.0
FirewallRuleName  : PowerShellForSysAdmins-FwRule

清单 12-22:创建 Azure SQL 服务器防火墙规则

就这样!你的数据库应该已经启动并运行了。

测试你的 SQL 数据库

要测试你的数据库,让我们创建一个小功能,使用System.Data.SqlClient.SqlConnection对象的Open()方法尝试一个简单的连接;请参见清单 12-23。

function Test-SqlConnection {
    param(
        [Parameter(Mandatory)]
     ❶ [string]$ServerName,

        [Parameter(Mandatory)]
        [string]$DatabaseName,

        [Parameter(Mandatory)]
     ❷ [pscredential]$Credential
    )

    try {
        $userName = $Credential.UserName
     ❸ $password = $Credential.GetNetworkCredential().Password
     ❹ $connectionString = 'Data Source={0};database={1};User
        ID={2};Password={3}' -f $ServerName,$DatabaseName,$userName,$password
        $sqlConnection = New-Object System.Data.SqlClient.SqlConnection
        $ConnectionString
     ❺ $sqlConnection.Open()
        $true
 } catch {
       if ($_.Exception.Message -match 'cannot open server') {
           $false
       } else {
           throw $_
       }
    } finally {
     ❻ $sqlConnection.Close()
    }
}

清单 12-23:测试连接到 Azure SQL 数据库的 SQL 连接

你使用之前创建的 SQL 服务器完全限定的域名作为此函数的ServerName参数❶,并将 SQL 管理员的用户名和密码放入一个PSCredential对象中❷。

然后,你将PSCredential对象分解成明文用户名和密码❸,创建连接字符串以建立数据库连接❹,在SqlConnection对象上调用Open()方法尝试连接到数据库❺,最后关闭数据库连接❻。

你可以通过运行 Test-SqlConnection -ServerName 'powershellsysadmins-sqlsrv.database.windows.net' -DatabaseName 'AutomateSQLDb' -Credential (Get-Credential) 来执行此功能。如果你能连接到数据库,函数将返回True;否则,它将返回False(并且需要进一步调查)。

你可以通过执行命令 Remove-AzResourceGroup -ResourceGroupName 'PowerShellForSysAdmins-SQL' 来清理所有内容。

总结

在这一章中,你深入学习了如何使用 PowerShell 自动化 Microsoft Azure。你设置了非交互式身份验证,部署了虚拟机、Web 应用和 SQL 数据库。而且,你全程都通过 PowerShell 完成,免去了访问 Azure 门户的麻烦。

如果没有Az PowerShell 模块和那些开发它的人的辛勤工作,你是做不到这一点的。像其他 PowerShell 云模块一样,所有这些命令都依赖于在后台调用的各种 API。多亏了这个模块,你不需要担心如何调用 REST 方法或使用端点 URL。

在下一章,你将学习如何使用 PowerShell 自动化 Amazon Web Services。

第十三章:与 AWS 合作

图片

在前一章中,你学习了如何使用 PowerShell 与 Microsoft Azure 进行操作。现在让我们看看如何使用 Amazon Web Services(AWS)。在本章中,你将深入学习如何使用 PowerShell 与 AWS 交互。首先,你将学习如何通过 PowerShell 认证 AWS,然后你将学习如何从零开始创建一个 EC2 实例,部署 Elastic Beanstalk(EBS)应用程序,并创建一个 Amazon Relational Database Service(Amazon RDS)Microsoft SQL Server 数据库。

像 Azure 一样,AWS 是云计算领域的巨头。如果你从事 IT 工作,你很可能会在职业生涯中以某种方式与 AWS 打交道。和 Azure 一样,AWS 也有一个方便的 PowerShell 模块:AWSPowerShell

你可以像安装Az模块一样,从 PowerShell Gallery 安装AWSPowerShell,只需调用 Install-Module AWSPowerShell。一旦该模块下载并安装完毕,你就可以开始使用了。

先决条件

我假设你已经有了一个 AWS 账户,并且可以访问根用户。你可以在* aws.amazon.com/free/* 注册一个 AWS 免费套餐账户。你不需要始终使用根用户,但你需要根用户来创建你的第一个身份和访问管理IAM)用户。你还需要下载并安装AWSPowerShell模块,正如之前所提到的。

AWS 认证

在 AWS 中,认证是通过 IAM 服务完成的,IAM 处理 AWS 中的认证、授权、计费和审计。要认证到 AWS,你必须在你的订阅下创建一个 IAM 用户,并且该用户必须能够访问相关资源。与 AWS 合作的第一步就是创建一个 IAM 用户。

当创建 AWS 账户时,会自动创建一个根用户,所以你将使用根用户来创建 IAM 用户。从技术上讲,你可以使用根用户在 AWS 中做任何事,但这强烈不推荐。

使用根用户进行认证

让我们创建一个你将在本章余下部分使用的 IAM 用户。然而,首先你需要以某种方式进行认证。如果没有其他 IAM 用户,唯一的方法就是使用根用户。遗憾的是,这意味着你暂时需要放弃 PowerShell。你需要使用 AWS 管理控制台的 GUI 来获取根用户的访问密钥和秘密密钥。

你的第一步是登录到 AWS 账户。导航到屏幕的右上角,点击账户下拉菜单,如图 13-1 所示。

图片

图 13-1:我的安全凭证选项

点击我的安全凭证选项。屏幕会弹出警告,提醒你修改安全凭证不是好主意;参见图 13-2。但是你需要在这里进行操作,以便创建一个 IAM 用户。

图片

图 13-2:认证警告

点击继续到安全凭证,然后点击访问密钥。点击创建新访问密钥应该会显示查看帐户访问密钥 ID 和密钥的方式。它还会提供下载一个包含这两者的密钥文件的选项。如果你还没有下载,下载该文件并将其保存在安全位置。现在,你需要从此页面复制访问密钥和秘密密钥,并将它们添加到 PowerShell 会话中的默认配置文件中。

将这两个密钥传递给Set-AWSCredential命令,它会保存这些密钥,以便在后续创建 IAM 用户的命令中重用。查看列出 13-1 以查看完整的命令。

PS> Set-AWSCredential -AccessKey 'access key' -SecretKey 'secret key'

列出 13-1:设置 AWS 访问密钥

完成这些步骤后,你已经准备好创建 IAM 用户。

创建 IAM 用户和角色

现在你已使用根用户身份验证,可以创建 IAM 用户。使用New-IAMUser命令,指定你想要使用的 IAM 用户名(在这个示例中是Automator)。当你创建用户时,你应该会看到类似列出 13-2 的输出。

PS> New-IAMUser -UserName Automator

Arn                 : arn:aws:iam::013223035658:user/Automator
CreateDate          : 9/16/2019 5:01:24 PM
PasswordLastUsed    : 1/1/0001 12:00:00 AM
Path                : /
PermissionsBoundary :
UserId              : AIDAJU2WN5KIFOUMPDSR4
UserName            : Automator

列出 13-2:创建 IAM 用户

注意在列出 13-2 中的Arn属性。在你创建 IAM 角色时,你需要使用这个值。

下一步是赋予用户适当的权限。你可以通过为该用户分配一个角色来完成此操作,角色已经分配了一个策略。AWS 将某些权限分组为称为角色的单位,这使得管理员可以更轻松地委派权限(这是一种称为基于角色的访问控制,或RBAC的策略)。策略则决定了角色可以访问哪些权限。

你可以通过使用New-IAMRole命令来创建一个角色,但首先你需要创建 AWS 所称的信任关系策略文档:一个 JSON 格式的文本字符串,定义了此用户可以访问的服务及其访问级别。

列出 13-3 是一个信任关系策略文档的示例。重要提示:注意 Principal 行中的 XXXXXX。请确保将你刚创建的 IAM 用户的 ARN 替换到这里。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal" : { "AWS": "arn:aws:iam::XXXXXX:user/Automator" },
            "Action": "sts:AssumeRole"
        }
    ]
}

列出 13-3:示例信任策略文档

这个 JSON 会更改角色本身(修改其信任策略),以允许你的Automator用户使用它。它是将AssumeRole权限赋予你的用户。这是创建角色所必需的。如需了解如何创建信任关系策略文档的更多信息,请参阅https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html

将此 JSON 字符串赋值给$json变量,然后将其作为AssumeRolePolicyDocument参数的值传递给New-IamRole,如列出 13-4 所示。

PS> $json = '{
>> "Version": "2012-10-17",
>> "Statement": [
>> {
>> "Effect": "Allow",
>> "Principal" : { "AWS": "arn:aws:iam::XXXXXX:user/Automator" },
>> "Action": "sts:AssumeRole"
>> }
>> ]
>> }'
PS> New-IAMRole -AssumeRolePolicyDocument $json -RoleName 'AllAccess'

Path             RoleName                         RoleId                   CreateDate          
----             --------                         ------                   ----------          
/                AllAccess                        <Your Specific Role ID>  <Date created>

列出 13-4:创建新 IAM 角色

现在 IAM 角色已经创建,你需要赋予它访问你将使用的各种资源的权限。与其花费接下来的几十页篇幅详细讲解 AWS IAM 角色和安全性,不如做一件简单的事,给Automator授予对所有内容的完全访问权限(实际上将其变成根用户)。

请注意,在实际操作中,你应该这样做。最佳实践是尽量限制访问权限,仅授权必要的权限。有关更多信息,请参考 AWS IAM 最佳实践指南(* docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html *)。但现在,让我们通过使用Register-IAMUserPolicy命令为此用户分配AdministratorAccess托管策略。你需要该策略的 Amazon 资源名称(ARN)。为此,你可以使用Get-IAMPolicies命令,通过策略名称进行筛选,将名称存储在变量中,然后将该变量传递到Register-IAMUserPolicy(所有操作可以参考示例 13-5)。

PS> $policyArn = (Get-IAMPolicies | where {$_.PolicyName -eq 'AdministratorAccess'}).Arn
PS> Register-IAMUserPolicy -PolicyArn $policyArn -UserName Automator

示例 13-5:将策略附加到用户

你需要做的最后一件事是生成一个访问密钥,用以验证你的用户。你可以使用New-IAMAcessKey命令来完成这项工作,正如示例 13-6 所示。

PS> $key = New-IAMAccessKey -UserName Automator
PS> $key

AccessKeyId     : XXXXXXXX
CreateDate      : 9/16/2019 6:17:40 PM
SecretAccessKey : XXXXXXXXX
Status          : Active
UserName        : Automator

示例 13-6:创建 IAM 访问密钥

你的新 IAM 用户已经设置好了。现在让我们对其进行身份验证。

身份验证你的 IAM 用户

在前面的章节中,你已经使用了根用户进行身份验证——这只是一个临时措施。你需要验证你的 IAM 用户,以便实际开始工作!在 AWS 中,几乎所有操作都需要先验证 IAM 用户。在这里,你将再次使用Set-AWSCredential命令,通过新访问密钥和秘密密钥更新你的配置文件。然而,这次需要稍微修改命令,使用StoreAs参数,正如示例 13-7 所示。由于你将在剩下的会话中使用该 IAM 用户,因此你将把访问密钥和秘密密钥存储在 AWS 默认配置文件中,这样每次会话就不需要再次运行此命令了。

PS> Set-AWSCredential -AccessKey $key.AccessKeyId -SecretKey 
$key.SecretAccessKey -StoreAs 'Default'

示例 13-7:设置默认 AWS 访问密钥

最后一个命令是Initialize-AWSDefaultConfiguration -Region 'your region here',它可以避免每次调用命令时都必须指定区域。这是一次性步骤。你可以通过运行Get-AWSRegion命令来查找所有区域,并找出离你最近的区域。

就是这样!你现在已经在 AWS 中完成了身份验证,可以开始使用 AWS 服务了。为了确认,可以运行Get-AWSCredentials命令并使用ListProfileDetail参数查看所有已保存的凭证。如果一切正常,你将看到默认配置文件显示出来:

PS> Get-AWSCredentials -ListProfileDetail
ProfileName StoreTypeName         ProfileLocation
----------- -------------         ---------------
Default     NetSDKCredentialsFile

创建 AWS EC2 实例

在第十二章中,你创建了一个 Azure 虚拟机。在这里,你将做类似的事情,通过创建AWS EC2 实例。AWS EC2 实例提供与 Azure 虚拟机相同的学习机会;无论是在 Azure 还是 AWS 中,创建虚拟机是极其常见的事情。然而,在 AWS 中创建虚拟机时,你需要以不同于 Azure 的方式来进行资源配置。在这里,底层 API 不同,这意味着你运行的命令会有所不同,但简而言之,你将执行基本相同的任务:创建一个虚拟机。AWS 还有自己的术语,这也让人有些困惑!我尽量让步骤尽可能地与前一章中创建虚拟机的步骤相似,但由于 Azure 和 AWS 在架构和语法上的差异,你会看到一些明显的不同。

幸运的是,和 Azure 一样,你有一个名为AWSPowerShell的模块,它可以让你更轻松地从零开始编写所有内容。就像在前一章中做的那样,你将从头开始:设置所有所需的依赖项,然后创建 EC2 实例。

虚拟私有云

第一个依赖是网络。你可以使用现有的网络或自己构建一个。因为本书是实践型的,你将从零开始构建自己的网络。在 Azure 中,你使用的是 vNet,但在 AWS 中,你将使用虚拟私有云(VPCs),它是一种网络架构,使虚拟机能够连接到云的其他部分。为了复制 Azure vNet 可能具有的相同设置,你只需创建一个具有单个子网的 VPC,并将其设置为最基础的级别。由于有许多配置选项可以选择,我决定最好尽可能地模拟我们的 Azure 网络配置。

在开始之前,你需要知道要创建的子网。让我们以 10.10.0.0/24 作为我们的示例网络。你将存储该信息和一个变量,并使用New-EC2Vpc命令,如清单 13-8 所示。

PS> $network = '10.0.0.0/16'
PS> $vpc = New-EC2Vpc -CidrBlock $network
PS> $vpc

CidrBlock                   : 10.0.0.0/24
CidrBlockAssociationSet     : {vpc-cidr-assoc-03f1edbc052e8c207}
DhcpOptionsId               : dopt-3c9c3047
InstanceTenancy             : default
Ipv6CidrBlockAssociationSet : {}
IsDefault                   : False
State                       : pending
Tags                        : {}
VpcId                       : vpc-03e8c773094d52eb3

清单 13-8:创建 AWS VPC

创建 VPC 后,你必须手动启用 DNS 支持(Azure 会自动为你完成此操作)。手动启用 DNS 支持应该将连接到该 VPC 的服务器指向一个内部的 Amazon DNS 服务器。同样,你需要手动提供一个公共主机名(这是 Azure 为你完成的另一项任务)。要做到这一点,你需要启用 DNS 主机名。通过使用清单 13-9 中的代码来完成这两项操作。

PS> Edit-EC2VpcAttribute -VpcId $vpc.VpcId -EnableDnsSupport $true
PS> Edit-EC2VpcAttribute -VpcId $vpc.VpcId -EnableDnsHostnames $true

清单 13-9:启用 VPC DNS 支持和主机名

请注意,你在两者中都使用了Edit-EC2VpcAttribute命令。顾名思义,该命令让你编辑 EC2 VPC 的多个属性。

Internet 网关

下一步是创建一个互联网网关。这将允许你的 EC2 实例将流量路由到互联网并从互联网接收流量。你需要手动执行此操作,这里使用的是New-EC2InternetGateway命令(列表 13-10)。

PS> $gw = New-EC2InternetGateway
PS> $gw

Attachments InternetGatewayId     Tags
----------- -----------------     ----
{}          igw-05ca5aaa3459119b1 {}

列表 13-10:创建互联网网关

创建了网关后,你必须使用Add-EC2InternetGateway命令将其附加到 VPC 中,正如列表 13-11 所示。

PS> Add-EC2InternetGateway -InternetGatewayId $gw.InternetGatewayId -VpcId $vpc.VpcId

列表 13-11:将 VPC 附加到互联网网关

在 VPC 处理完后,让我们进入下一步,为你的网络添加路由。

路由

在创建了网关之后,你现在需要创建一个路由表和一个路由,以便 VPC 上的 EC2 实例可以访问互联网。路由是网络流量到达目的地所经过的路径。路由表是一个,嗯,路由的表格。你的路由需要放在一个表格里,所以你会先创建路由表。使用New-EC2RouteTable命令,传入你的 VPC ID(列表 13-12)。

PS> $rt = New-EC2RouteTable -VpcId $vpc.VpcId
PS> $rt

Associations    : {}
PropagatingVgws : {}
Routes          : {}
RouteTableId    : rtb-09786c17af32005d8
Tags            : {}
VpcId           : vpc-03e8c773094d52eb3

列表 13-12:创建路由表

在路由表内,你创建一个指向刚刚创建的网关的路由。你正在创建一个默认路由,或者叫默认网关,意味着如果没有定义更具体的路由,外发的网络流量将通过该路由。你将所有流量(0.0.0.0/0)都通过你的互联网网关进行路由。使用New-EC2Route命令,如果成功返回True,正如列表 13-13 所示。

PS> New-EC2Route -RouteTableId $rt.RouteTableId -GatewayId 
$gw.InternetGatewayId -DestinationCidrBlock '0.0.0.0/0'

True

列表 13-13:创建路由

如你所见,路由应该已成功创建!

子网

接下来,你需要在更大的 VPC 中创建一个子网,并将其与路由表关联。记住,子网定义了你的 EC2 实例的网络适配器将参与的逻辑网络。为了创建一个子网,你可以使用New-EC2Subnet命令,然后使用Register-EC2RouteTable命令将子网注册到之前创建的路由表中。首先,你需要为子网定义一个可用区(AWS 数据中心将托管你的子网的位置)。如果你不确定要使用哪个可用区,可以使用Get-EC2AvailabilityZone命令列出所有可用区。列表 13-14 显示了你执行此操作时的情况。

PS> Get-EC2AvailabilityZone

Messages RegionName State     ZoneName
-------- ---------- -----     --------
{}       us-east-1  available us-east-1a
{}       us-east-1  available us-east-1b
{}       us-east-1  available us-east-1c
{}       us-east-1  available us-east-1d
{}       us-east-1  available us-east-1e
{}       us-east-1  available us-east-1f

列表 13-14:列举 EC2 可用区

如果你不介意的话,我们使用us-east-1d可用区。列表 13-15 显示了使用New-EC2Subnet命令创建子网的代码,该命令需要你之前创建的 VPC ID,一个 CIDR 块(子网),以及你找到的可用区,最后是注册路由表的代码(使用Register-EC2RouteTable命令)。

PS> $sn = New-EC2Subnet -VpcId $vpc.VpcId -CidrBlock '10.0.1.0/24' -AvailabilityZone 'us-east-1d'
PS> Register-EC2RouteTable -RouteTableId $rt.RouteTableId -SubnetId $sn.SubnetId
rtbassoc-06a8b5154bc8f2d98

列表 13-15:创建并注册子网

现在你已经创建并注册了子网,网络栈的工作就完成了!

将 AMI 分配给你的 EC2 实例

在构建网络堆栈之后,你需要为你的虚拟机分配一个 Amazon Machine Image(AMI)。AMI是一个磁盘的“快照”,用于作为模板,防止每次都要从头开始安装操作系统。你需要找到一个符合你需求的现有 AMI:你需要一个支持 Windows Server 2016 实例的 AMI,因此首先需要找到该实例的名称。使用Get-EC2ImageByName命令列出所有可用的实例,你应该会看到一个名为WINDOWS_2016_BASE的镜像。太完美了。

现在你已经知道了镜像的名称,重新使用Get-EC2ImageByName命令,这次指定你想使用的镜像。这样做会告诉命令返回你需要的镜像对象,正如你在列表 13-16 中看到的那样。

PS> $ami = Get-EC2ImageByName -Name 'WINDOWS_2016_BASE'
PS> $ami

Architecture        : x86_64
BlockDeviceMappings : {/dev/sda1, xvdca, xvdcb, xvdcc...}
CreationDate        : 2019-08-15T02:27:20.000Z
Description         : Microsoft Windows Server 2016...
EnaSupport          : True
Hypervisor          : xen
ImageId             : ami-0b7b74ba8473ec232
ImageLocation       : amazon/Windows_Server-2016-English-Full-Base-2019.08.15
ImageOwnerAlias     : amazon
ImageType           : machine
KernelId            :
Name                : Windows_Server-2016-English-Full-Base-2019.08.15
OwnerId             : 801119661308
Platform            : Windows
ProductCodes        : {}
Public              : True
RamdiskId           :
RootDeviceName      : /dev/sda1
RootDeviceType      : ebs
SriovNetSupport     : simple
State               : available
StateReason         :
Tags                : {}
VirtualizationType  : hvm

列表 13-16:查找 AMI

你的镜像已存储并准备就绪。最后,你可以创建你的 EC2 实例。你只需要指定实例类型;不幸的是,你无法通过 PowerShell cmdlet 获取实例类型的列表,但你可以在https://aws.amazon.com/ec2/instance-types/上找到它们。我们来使用免费的t2.micro。加载你的参数——镜像 ID、是否希望与公共 IP 关联、实例类型和子网 ID——然后运行New-EC2Instance命令(列表 13-17)。

PS> $params = @{
>> ImageId = $ami.ImageId
>> AssociatePublicIp = $false
>> InstanceType = 't2.micro'
>> SubnetId = $sn.SubnetId
}
PS> New-EC2Instance @params

GroupNames    : {}
Groups        : {}
Instances     : {}
OwnerId       : 013223035658
RequesterId   :
ReservationId : r-05aa0d9b0fdf2df4f

列表 13-17:创建 EC2 实例

完成了!你应该可以在 AWS 管理控制台中看到一个全新的 EC2 实例,或者你可以使用Get-EC2Instance命令来返回你新创建的实例。

总结

你已经搞定了创建 EC2 实例的代码,但目前的代码使用起来较为繁琐。让我们把这段代码改得更容易重复使用。创建 EC2 实例很可能是一个频繁的操作,因此你可以创建一个自定义函数,避免一步步手动执行。这个函数的高层工作原理与第十二章中在 Azure 创建的函数相同;我在这里不会详细讲解该函数的具体内容,但你可以在书本的资源中找到该脚本,我强烈建议你自己动手尝试构建这个函数。

当脚本被调用,并且所有依赖项已经存在,除了EC2 实例本身,你在运行带有Verbose参数时,将看到类似于列表 13-18 的输出。

PS> $parameters = @{
>> VpcCidrBlock = '10.0.0.0/16'
>> EnableDnsSupport = $true
>> SubnetCidrBlock = '10.0.1.0/24'
>> OperatingSystem = 'Windows Server 2016'
>> SubnetAvailabilityZone = 'us-east-1d'
>> InstanceType = 't2.micro'
>> Verbose = $true
}
PS> New-CustomEC2Instance @parameters

VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeVpcs' in region 'us-east-1'
VERBOSE: A VPC with the CIDR block [10.0.0.0/16] has already been created.
VERBOSE: Enabling DNS support on VPC ID [vpc-03ba701f5633fcfac]...
VERBOSE: Invoking Amazon EC2 operation 'ModifyVpcAttribute' in region 'us-east-1'
VERBOSE: Invoking Amazon EC2 operation 'ModifyVpcAttribute' in region 'us-east-1'
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeInternetGateways' in region 
         'us-east-1'
VERBOSE: An internet gateway is already attached to VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeRouteTables' in region 
         'us-east-1'
VERBOSE: Route table already exists for VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: A default route has already been created for route table ID [rtb-0b4aa3a0e1801311f 
         rtb-0aed41cac6175a94d].
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeSubnets' in region 'us-east-1'
VERBOSE: A subnet has already been created and registered with VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: Invoking Amazon EC2 operation 'DescribeImages' in region 'us-east-1'
VERBOSE: Creating EC2 instance...
VERBOSE: Invoking Amazon EC2 operation 'RunInstances' in region 'us-east-1'

GroupNames    : {}
Groups        : {}
Instances     : {}
OwnerId       : 013223035658
RequesterId   :
ReservationId : r-0bc2437cfbde8e92a

列表 13-18:运行自定义 EC2 实例创建函数

现在你拥有了自动化创建 AWS 中 EC2 实例这一枯燥任务的工具!

部署 Elastic Beanstalk 应用程序

类似于微软 Azure 的 Web 应用服务,AWS 也有自己的 Web 应用服务。Elastic Beanstalk (EB)是一项允许你上传 Web 包以托管在 AWS 基础设施上的服务。在本节中,你将看到创建 EB 应用程序并将包部署到其中所需的步骤。这个过程需要五个步骤:

  1. 创建应用程序。

  2. 创建环境。

  3. 上传包以使其可以用于应用程序。

  4. 创建一个新的应用程序版本。

  5. 将新版本部署到环境中。

让我们从创建一个新的应用程序开始。

创建应用程序

要创建一个新的应用程序,请使用New-EBApplication命令,提供应用程序的名称。我们将其命名为AutomateWorkflow。运行该命令后,你应该会看到类似于清单 13-19 的输出。

PS> $ebApp = New-EBApplication -ApplicationName 'AutomateWorkflow'
PS> $ebApp

ApplicationName         : AutomateWorkflow
ConfigurationTemplates  : {}
DateCreated             : 9/19/2019 11:43:56 AM
DateUpdated             : 9/19/2019 11:43:56 AM
Description             :
ResourceLifecycleConfig : Amazon.ElasticBeanstalk.Model
                          .ApplicationResourceLifecycleConfig
Versions                : {}

清单 13-19:创建一个新的 Elastic Beanstalk 应用程序

下一步是创建环境,即应用程序将托管的基础设施。创建新环境的命令是New-EBEnvironment。不幸的是,创建环境不像创建应用程序那样简单。应用程序名称和环境名称等参数由你决定,但你需要知道SolutionStackNameTier_TypeTier_Name。让我们更仔细地看看这些参数。

你使用SolutionStackName来指定你希望应用程序运行的操作系统和 IIS 版本。要查看可用的解决方案栈,请运行Get-EBAvailableSolutionStackList命令,并检查SolutionStackDetails属性,如清单 13-20 所示。

PS> (Get-EBAvailableSolutionStackList).SolutionStackDetails

PermittedFileTypes SolutionStackName
------------------ -----------------
{zip}              64bit Windows Server Core 2016 v1.2.0 running IIS 10.0
{zip}              64bit Windows Server 2016 v1.2.0 running IIS 10.0
{zip}              64bit Windows Server Core 2012 R2 v1.2.0 running IIS 8.5
{zip}              64bit Windows Server 2012 R2 v1.2.0 running IIS 8.5
{zip}              64bit Windows Server 2012 v1.2.0 running IIS 8
{zip}              64bit Windows Server 2008 R2 v1.2.0 running IIS 7.5
{zip}              64bit Amazon Linux 2018.03 v2.12.2 runni...
{jar, zip}         64bit Amazon Linux 2018.03 v2.7.4 running Java 8
{jar, zip}         64bit Amazon Linux 2018.03 v2.7.4 running Java 7
{zip}              64bit Amazon Linux 2018.03 v4.5.3 running Node.js
{zip}              64bit Amazon Linux 2015.09 v2.0.8 running Node.js
{zip}              64bit Amazon Linux 2015.03 v1.4.6 running Node.js
{zip}              64bit Amazon Linux 2014.03 v1.1.0 running Node.js
{zip}              32bit Amazon Linux 2014.03 v1.1.0 running Node.js
{zip}              64bit Amazon Linux 2018.03 v2.8.1 running PHP 5.4
--snip--

清单 13-20:查找可用的解决方案栈

如你所见,你有很多选择。对于这个示例,选择 64 位的 Windows Server Core 2012 R2,并运行 IIS 8.5。

现在让我们看一下Tier_TypeTier_Type指定了你的 Web 服务将运行的环境类型。如果你打算使用此环境来托管一个网站,则必须选择Standard类型。

最后,对于Tier_Name参数,你有WebServerWorker两个选项。这里选择WebServer,因为你希望托管一个网站(如果创建的是 API,则需要选择Worker)。

现在所有参数都已确定,让我们运行New-EBEnvironment。清单 13-21 显示了命令及其输出。

PS> $instanceProfileOptionSetting = New-Object Amazon.ElasticBeanstalk.Model.
ConfigurationOptionSetting -ArgumentList aws:autoscaling:launchconfiguration,
IamInstanceProfile,'aws-elasticbeanstalk-ec2-role'

>> $parameters = @{
>>      ApplicationName = 'AutomateWorkflow'
>>      EnvironmentName = 'Testing'
>> SolutionStackName = '64bit Windows Server Core 2019 v2.5.9 running IIS 10.0'
>>      Tier_Type = 'Standard'
>>      Tier_Name = 'WebServer'
>>      OptionSetting = $instanceProfileOptionSetting
>> }
PS> New-EBEnvironment @parameters

AbortableOperationInProgress : False
ApplicationName              : AutomateWorkflow
CNAME                        :
DateCreated                  : 10/3/2020 9:31:49 AM
DateUpdated                  : 10/3/2020 9:31:49 AM
Description                  :
EndpointURL                  :
EnvironmentArn               : arn:aws:elasticbeanstalk:us-east-1:054715970076:environment/AutomateWorkflow/Testing 
EnvironmentId                :  e-f3pfgxhrzf
EnvironmentLinks             : {}
EnvironmentName              : Testing
Health                       : Grey
HealthStatus                 :
OperationsRole               :
PlatformArn                  : arn:aws:elasticbeanstalk:us-east-1::platform/IIS 10.0 running on 64bit Windows Server Core 
                               2019/2.5.9 
Resources                    :
SolutionStackName            : 64bit Windows Server Core 2019 v2.5.9 running IIS 10.0
Status                       : Launching
TemplateName                 :
Tier                         : Amazon.ElasticBeanstalk.Model.EnvironmentTier
VersionLabel                 :

清单 13-21:创建一个 Elastic Beanstalk 应用程序

你会注意到状态显示为Launching。这意味着应用程序尚未可用,因此你可能需要稍等片刻,直到环境启动。你可以通过运行Get-EBEnvironment -ApplicationName 'AutomateWorkflow' -EnvironmentName 'Testing'定期检查应用程序的状态。环境可能会在Launching状态下持续几分钟。

当你看到Status属性变为Ready时,环境已经启动,接下来可以将包部署到网站。

部署一个包

现在开始部署。你要部署的包应该包含你希望网站托管的所有文件。你可以在其中放入任何文件——为了我们的目的,文件内容无关紧要。唯一需要确保的是它是一个 ZIP 文件。使用Compress-Archive命令将你想部署的文件打包:

PS> Compress-Archive -Path 'C:\MyPackageFolder\*' -DestinationPath 'C:\package.zip'

将你的包裹整理好并压缩后,你需要把它放在一个应用程序可以找到的地方。你可以把它放在几个地方,但在这个例子中,你将它放入一个 Amazon S3 存储桶,这是 AWS 中常用的数据存储方式。要将文件放入 Amazon S3 存储桶,首先你需要一个 Amazon S3 存储桶!让我们在 PowerShell 中创建一个。继续运行命令 New-S3Bucket -BucketName 'automateworkflow'

现在你的 S3 存储桶已经准备好等待文件内容,使用 Write-S3Object 命令上传 ZIP 文件,如清单 13-22 所示。

PS> Write-S3Object -BucketName 'automateworkflow' -File 'C:\package.zip'

清单 13-22:将包上传到 S3

现在你必须指向你刚刚创建的 S3 密钥,并为其指定一个版本标签。版本标签可以是任何内容,但通常你会使用一个基于时间的唯一数字。所以我们使用表示当前日期和时间的刻度数。一旦你有了版本标签,运行 New-EBApplicationVersion 并加入更多参数,如清单 13-23 所示。

PS> $verLabel = [System.DateTime]::Now.Ticks.ToString()
PS> $newVerParams = @{
>>      ApplicationName       = 'AutomateWorkflow'
>>      VersionLabel          = $verLabel
>>      SourceBundle_S3Bucket = 'automateworkflow'
>>      SourceBundle_S3Key    = 'package.zip'
}
PS> New-EBApplicationVersion @newVerParams

ApplicationName        : AutomateWorkflow
BuildArn               :
DateCreated            : 9/19/2019 12:35:21 PM
DateUpdated            : 9/19/2019 12:35:21 PM
Description            :
SourceBuildInformation :
SourceBundle           : Amazon.ElasticBeanstalk.Model.S3Location
Status                 : Unprocessed
VersionLabel           : 636729573206374337

清单 13-23:创建新的应用程序版本

你的应用程序版本现在已经创建完成!现在是时候将这个版本部署到你的环境中了。通过使用 Update-EBEnvironment 命令来完成这个操作,如清单 13-24 所示。

PS> Update-EBEnvironment -ApplicationName 'AutomateWorkflow'  -EnvironmentName 
'Testing' -VersionLabel $verLabel -Force

AbortableOperationInProgress : True
ApplicationName              : AutomateWorkflow
CNAME                        : Testing.3u2ukxj2ux.us-ea...
DateCreated                  : 9/19/2019 12:19:36 PM
DateUpdated                  : 9/19/2019 12:37:04 PM
Description                  :
EndpointURL                  : awseb-e-w-AWSEBL...
EnvironmentArn               : arn:aws:elasticbeanstalk... 
EnvironmentId                : e-wkba2k4kcf
EnvironmentLinks             : {}
EnvironmentName              : Testing
Health                       : Grey
HealthStatus                 :
PlatformArn                  : arn:aws:elasticbeanstalk:... 
Resources                    :
SolutionStackName            : 64bit Windows Server Core 2012 R2 running IIS 8.5
Status                       : ❶Updating
TemplateName                 :
Tier                         : Amazon.ElasticBeanstalk.Model.EnvironmentTier
VersionLabel                 : 636729573206374337

清单 13-24:将应用程序部署到 EB 环境

你可以看到状态已经从Ready变为Updating ❶。同样,你需要等一会,直到状态变回Ready,正如在清单 13-25 中看到的那样。

PS> Get-EBEnvironment -ApplicationName 'AutomateWorkflow' 
-EnvironmentName 'Testing'

AbortableOperationInProgress : False
ApplicationName              : AutomateWorkflow
CNAME                        : Testing.3u2ukxj2ux.us-e...
DateCreated                  : 9/19/2019 12:19:36 PM
DateUpdated                  : 9/19/2019 12:38:53 PM
Description                  :
EndpointURL                  : awseb-e-w-AWSEBL...
EnvironmentArn               : arn:aws:elasticbeanstalk... 
EnvironmentId                : e-wkba2k4kcf
EnvironmentLinks             : {}
EnvironmentName              : Testing
Health                       : Green
HealthStatus                 :
PlatformArn                  : arn:aws:elasticbeanstalk:...
Resources                    :
SolutionStackName            : 64bit Windows Server Core 2012 R2 running IIS 8.5
Status                       : ❶Ready
TemplateName                 :
Tier                         : Amazon.ElasticBeanstalk.Model.EnvironmentTier
VersionLabel                 :

清单 13-25:确认应用程序已准备好

在你检查时,状态再次变为Ready ❶。一切看起来都很好!

在 AWS 中创建 SQL Server 数据库

作为 AWS 管理员,你可能需要设置不同类型的关系数据库。AWS 提供了 Amazon 关系数据库服务(Amazon RDS),它使管理员可以轻松地配置几种类型的数据库。有几种选择,但现在,你将专注于 SQL。

在这一部分,你将创建一个空白的 Microsoft SQL Server 数据库在 RDS 中。你将使用的主要命令是 New-RDSDBInstance。像 New-AzureRmSqlDatabase 一样,New-RDSDBInstance许多 参数,远超过我在这一部分能覆盖的。如果你对其他配置 RDS 实例的方式感兴趣,我鼓励你查看 New-RDSDBInstance 的帮助文档。

对于我们的目的,你需要以下信息:

  • 实例的名称

  • 数据库引擎(SQL Server、MariaDB、MySQL 等)

  • 实例类别,指定 SQL Server 运行所需的资源类型

  • 主用户名和密码

  • 数据库的大小(以 GB 为单位)

有些信息你可以很容易地搞明白:名称、用户名/密码和大小。其他的则需要进一步调查。

让我们从引擎版本开始。你可以使用 Get-RDSDBEngineVersion 命令获取所有可用引擎及其版本的列表。此命令在没有参数的情况下运行时,会返回大量信息——对于你正在做的事情来说,信息过多。你可以使用 Group-Object 命令按引擎对所有对象进行分组,这将提供按引擎名称分组的所有引擎版本的列表。正如你在 列表 13-26 中看到的,你现在有一个更易管理的输出,显示了你可以使用的所有可用引擎。

PS> Get-RDSDBEngineVersion | Group-Object -Property Engine

Count Name                      Group
----- ----                      -----
    1 aurora-mysql              {Amazon.RDS.Model.DBEngineVersion}
    1 aurora-mysql-pq           {Amazon.RDS.Model.DBEngineVersion}
    1 neptune                   {Amazon.RDS.Model.DBEngineVersion}
--snip--
   16 sqlserver-ee              {Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Mo...

   17 sqlserver-ex              {Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Mo...

   17 sqlserver-se              {Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Mo...

   17 sqlserver-web             {Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Model.DBEngineVersion, 
                                Amazon.RDS.Mo...
--snip--

列表 13-26:调查 RDS 数据库引擎版本

你有四个 sqlserver 条目,分别代表 SQL Server Express、Web 版、标准版和企业版。由于这只是一个示例,你将选择 SQL Server Express;它是一个简洁的数据库引擎,最重要的是,它是免费的,这使得你可以在必要时对其进行调优和调整。通过使用 sqlserver-ex 选择 SQL Server Express 引擎。

选择引擎后,你需要指定一个版本。默认情况下,New-RDSDBInstance 会配置最新版本(即你将使用的版本),但你可以通过使用 EngineVersion 参数指定不同的版本。要查看所有可用版本,你需要再次运行 Get-RDSDBEngineVersion,将搜索限制为 sqlserver-ex,并且只返回引擎版本(参见 列表 13-27)。

PS> Get-RDSDBEngineVersion -Engine 'sqlserver-ex' | 
Format-Table -Property EngineVersion

EngineVersion
-------------
10.50.6000.34.v1
10.50.6529.0.v1
10.50.6560.0.v1
11.00.5058.0.v1
11.00.6020.0.v1
11.00.6594.0.v1
11.00.7462.6.v1
12.00.4422.0.v1
12.00.5000.0.v1
12.00.5546.0.v1
12.00.5571.0.v1
13.00.2164.0.v1
13.00.4422.0.v1
13.00.4451.0.v1
13.00.4466.4.v1
14.00.1000.169.v1
14.00.3015.40.v1

列表 13-27:查找 SQL Server Express 引擎版本

接下来,你需要为 New-RDSDBInstance 提供实例类的值。实例类代表托管数据库的基础架构性能——包括内存、CPU 等。不幸的是,没有 PowerShell 命令可以轻松找到所有可用的实例类选项,但你可以查看此链接以获取完整信息:https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html

在选择实例类时,重要的是要验证它是否被你选择的引擎所支持。在这里,你将使用 db2.t2.micro 实例类来创建你的 RDS 数据库,但许多其他选项将无法使用。有关哪些实例类在不同的 RDS 数据库下受支持的详细信息,请参见 AWS RDS 常见问题解答 (aws.amazon.com/rds/faqs/)。如果你选择了一个不被你使用的引擎支持的实例类,你将收到类似 列表 13-28 中的错误消息。

New-RDSDBInstance : RDS does not support creating a DB instance with the 
following combination: DBInstanceClass=db.t1.micro, Engine=sqlserver-ex, 
EngineVersion=14.00.3015.40.v1, LicenseModel=license-included. For supported 
combinations of instance class and database engine version, see the 
documentation.

列表 13-28:指定无效实例配置时的错误

一旦选择了(受支持的)实例类,你需要决定一个用户名和密码。请注意,AWS 不接受任何旧密码:密码中不能包含斜杠、@ 符号、逗号或空格,否则你将收到类似 列表 13-29 中的错误消息。

New-RDSDBInstance : The parameter MasterUserPassword is not a valid password. 
Only printable ASCII characters besides '/', '@', '"', ' ' may be used.

清单 13-29:使用 New-RDSDBInstance 指定无效密码

有了这些,您就具备了启动 New-RDSDBInstance 所需的所有参数!您可以在 清单 13-30 中查看预期的输出。

PS> $parameters = @{
>>      DBInstanceIdentifier = 'Automating'
>>      Engine = 'sqlserver-ex'
>>      DBInstanceClass = 'db.t2.micro'
>>      MasterUsername = 'sa'
>>      MasterUserPassword = 'password'
>>      AllocatedStorage = 20
}
PS> New-RDSDBInstance @parameters

AllocatedStorage                      : 20
AutoMinorVersionUpgrade               : True
AvailabilityZone                      :
BackupRetentionPeriod                 : 1
CACertificateIdentifier               : rds-ca-2015
CharacterSetName                      :
CopyTagsToSnapshot                    : False
--snip--

清单 13-30:配置一个新的 RDS 数据库实例

恭喜!您的 AWS 应该已经拥有一个崭新的 RDS 数据库。

总结

本章介绍了使用 AWS 和 PowerShell 的基础知识。您学习了 AWS 的身份验证,然后逐步了解了几个常见的 AWS 任务:创建 EC2 实例、部署 Elastic Beanstalk Web 应用程序,以及配置 Amazon RDS SQL 数据库。

在本章以及前一章之后,您应该已经对如何使用 PowerShell 操作云有了一个清晰的了解。当然,还有更多内容需要学习——远远超出了我在本书中能够涉及的范围——但现在,您将进入本书的下一部分:创建您自己的完全功能的 PowerShell 模块。

第十四章:创建服务器清单脚本

Images

到目前为止,在本书中,你已经专注于学习 PowerShell 作为一种语言,熟悉其语法和命令。但 PowerShell 不仅仅是一个语言,它还是一个工具。既然你已经掌握了 PowerShell 的基本知识,现在是时候进行更有趣的部分了!

PowerShell 的真正力量在于它的工具制作能力。在这个上下文中,工具指的是一个 PowerShell 脚本、一个模块、一个函数或任何有助于你执行管理任务的东西。无论任务是创建报告、收集计算机信息、创建公司用户账户,还是更复杂的任务,你都将学习如何使用 PowerShell 自动化这些任务。

在本章中,我将向你展示如何使用 PowerShell 收集数据,以便做出更明智的决策。具体来说,你将构建一个服务器清单项目。你将学习如何创建一个带有参数的脚本,输入服务器名称,并发现大量的信息供你浏览:操作系统规格以及硬件信息,包括存储大小、空闲存储、内存等。

先决条件

在开始本章之前,你需要一台已加入域的 Windows 计算机、对 Active Directory 计算机对象的读取权限、一个包含计算机账户的 Active Directory 组织单位(OU),以及可以从 https://www.microsoft.com/en-us/download/details.aspx?id=45520 下载的远程服务器管理工具包(RSAT)。

创建项目脚本

由于你将在本章中构建脚本,而不仅仅是在控制台中执行代码,首先你需要创建一个新的 PowerShell 脚本。创建一个名为 Get-ServerInformation.ps1 的脚本。我把我的脚本放在 *C:* 目录下。你将在本章中不断地往这个脚本中添加代码。

定义最终输出

在你开始编写代码之前,制定一个“草图”计划,确定完成后输出应该是什么样子,这是一个良好的实践。这个简单的草图是衡量进度的一个好方法,尤其是在构建大型脚本时。

对于这个服务器清单脚本,我们假设在脚本结束时,你希望在 PowerShell 控制台中看到如下输出:

ServerName  IPAddress  OperatingSystem  AvailableDriveSpace (GB)  Memory (GB)  UserProfilesSize (MB)  StoppedServices
MYSERVER    x.x.x.x    Windows....      10                        4            50.4                   service1,service2,service3

现在你知道你想看到的内容,让我们开始实现它。

发现与脚本输入

第一步是决定如何告诉你的脚本查询内容。你将从多个服务器收集信息。如在“先决条件”部分所述,你将使用 Active Directory 来查找服务器名称。

当然,你可以从文本文件中查询服务器名称,从存储在 PowerShell 脚本中的服务器名称数组中查询,从注册表中查询,从 Windows 管理工具 (WMI) 库中查询,或者从数据库中查询——这都没关系。只要你的脚本最终获得一个表示服务器名称的字符串数组,你就可以继续进行。不过,在这个项目中,你将使用来自 Active Directory 的服务器。

在这个示例中,所有的服务器都位于同一个 OU。如果你自己尝试时发现它们不在同一个 OU 中,也没关系;你只需要遍历你的 OU,并读取每个 OU 中的计算机对象即可。但在这里,你的第一个任务是读取 OU 中的所有计算机对象。在这个环境中,所有的服务器都位于Servers OU。你的域名是powerlab.local。要从 AD 中检索计算机对象,使用Get-ADComputer命令,如 Listing 14-1 所示。这个命令应该会返回你感兴趣的所有 AD 计算机对象。

PS> $serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
PS> $servers = Get-ADComputer -SearchBase $serversOuPath -Filter *
PS> $servers

DistinguishedName : CN=SQLSRV1,OU=Servers,DC=Powerlab,DC=local
DNSHostName       : SQLSRV1.Powerlab.local
Enabled           : True
Name              : SQLSRV1
ObjectClass       : computer
ObjectGUID        : c288d6c1-56d4-4405-ab03-80142ac04b40
SamAccountName    : SQLSRV1$
SID               : S-1-5-21-763434571-1107771424-1976677938-1105
UserPrincipalName :

DistinguishedName : CN=WEBSRV1,OU=Servers,DC=Powerlab,DC=local
DNSHostName       : WEBSRV1.Powerlab.local
Enabled           : True
Name              : WEBSRV1
ObjectClass       : computer
ObjectGUID        : 3bd2da11-4abb-4eb6-9c71-7f2c58594a98
SamAccountName    : WEBSRV1$
SID               : S-1-5-21-763434571-1107771424-1976677938-1106
UserPrincipalName :

Listing 14-1:使用Get-AdComputer返回服务器数据

注意,在这里你不是直接设置SearchBase参数的值,而是定义了一个变量。你应该习惯这样做。事实上,每当你遇到类似的具体配置时,把它放到一个变量中总是一个好主意,因为你永远不知道什么时候你还需要再次使用这个值。你还将Get-ADComputer的输出返回到一个变量中。由于你稍后还会处理这些服务器,因此你希望能够引用它们的名称。

Get-ADComputer命令返回的是整个 AD 对象,但你只需要服务器名称。你可以通过使用Select-Object来缩小范围,仅返回Name属性:

PS> $servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
PS> $servers
SQLSRV1
WEBSRV1

现在你已经有了查询单个服务器的基本思路,让我们来看一下如何查询所有服务器。

查询每台服务器

要查询每台服务器,你需要创建一个循环,这样可以确保每台服务器在你的数组中只被查询一次。

假设你的代码会立即工作通常并不是一个好主意(它通常不会)。相反,我喜欢在构建过程中慢慢进行,并在每个步骤中进行测试。在这种情况下,不要尝试一次性完成所有任务,而是使用Write-Host确保脚本返回你预期的服务器名称:

foreach ($server in $servers) {
    Write-Host $server
}

到现在为止,你应该已经有了一个名为Get-ServerInformation.ps1的脚本,内容如 Listing 14-2 所示。

$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * | Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    Write-Host $server
}

Listing 14-2:到目前为止你的脚本

一旦你运行脚本,你将获得一些服务器名称。根据你使用的服务器不同,输出可能会有所不同:

PS> C:\Get-ServerInformation.ps1
SQLSRV1
WEBSRV1

很好!你已经设置了一个循环,它会遍历你数组中的每个服务器名称。你的第一个任务已经完成。

提前思考:结合不同类型的信息

PowerShell 成功的关键之一是良好的规划和组织。部分内容就是了解预期结果。对于许多初学者来说,他们没有太多关于 PowerShell 可能返回的结果的经验,这是一个问题:他们知道自己希望发生什么(希望如此),但他们不知道可能发生什么。因此,他们编写的脚本会在数据源之间“之”字形地穿梭,从一个获取数据,再到另一个,接着是第一个,然后是第三个,将它们连接起来,再做一遍。其实有更简单的方式,我如果不暂停来解释这些,反而会对你造成不利影响。

查看 Listing 14-1 中的输出,你可以看到,你将需要一些命令来从不同的来源提取信息(WMI、文件系统、Windows 服务)。每个来源将返回不同类型的对象,如果你不加思考地将它们合并,你会得到一团糟。

稍微提前一点,让我们看一下,如果你尝试在没有任何格式化或输出关注的情况下提取服务名称和内存,输出会是什么样子。你可能会看到类似这样的内容:

Status   Name               DisplayName
------   ----               -----------
Running  wuauserv           Windows Update

__GENUS              : 2
__CLASS              : Win32_PhysicalMemory
__SUPERCLASS         : CIM_PhysicalMemory
__DYNASTY            : CIM_ManagedSystemElement
__RELPATH            : Win32_PhysicalMemory.Tag="Physical Memory 0"
__PROPERTY_COUNT     : 30
__DERIVATION         : {CIM_PhysicalMemory, CIM_Chip, CIM_PhysicalComponent, CIM_PhysicalElement...}
__SERVER             : DC
__NAMESPACE          : root\cimv2
__PATH               : \\DC\root\cimv2:Win32_PhysicalMemory.Tag="Physical Memory 0"

在这里,你正在查询一个服务,并同时尝试从服务器获取内存。这些对象不同,这些对象上的属性也不同,如果你将所有输出合并并直接输出,看起来会很糟糕。

让我们看看如何避免这种输出。由于你将组合不同类型的输出,并且你需要符合我们确切规范的内容,因此你必须创建自己类型的输出。别担心,这不像你想象的那么复杂。在第二章中,你学会了如何创建PSCustomObject类型。PowerShell 中的这些通用对象允许你添加自己的属性—非常适合你在这里做的事情。

你知道所需输出的标题(并且,正如我相信你现在已经知道的,这些“标题”将始终是对象属性)。让我们创建一个自定义对象,并将你希望在输出中看到的属性放进去。出于明显的原因,我将这个对象命名为$output;你在填充它的属性后将返回它:

$output = [pscustomobject]@{
    'ServerName'                  = $null
    'IPAddress'                   = $null
    'OperatingSystem'             = $null
    'AvailableDriveSpace (GB)'    = $null
    'Memory (GB)'                 = $null
    'UserProfilesSize (MB)'       = $null
    'StoppedServices'             = $null
}

你会注意到哈希表的键被单引号包围。如果键中没有空格,这是不强制的。然而,由于我在一些键名中使用了空格,我决定在所有键上统一使用单引号。通常不推荐在对象属性名称中使用空格,除非使用自定义格式化,但这超出了本书的范围。有关自定义格式化的更多信息,请参阅about_Format.ps1xml帮助主题。

如果你将其复制到控制台,并通过格式化命令Format-Table返回它,你将看到你所需要的标题:

PS> $output | Format-Table -AutoSize

ServerName IPAddress OperatingSystem AvailableDriveSpace (GB) Memory (GB) UserProfilesSize (MB) StoppedServices

Format-Table命令是 PowerShell 中少数几个格式化命令之一,旨在作为管道中的最后一个命令使用。它们会转换当前的输出并以不同的方式显示它。在这种情况下,你正在告诉 PowerShell 将对象输出转换为表格格式,并根据控制台的宽度自动调整行的大小。

一旦定义了自定义输出对象,你可以返回到循环中,确保每个服务器都以这种格式返回。由于你已经知道服务器名称,可以立即设置该属性,如 Listing 14-3 所示。

$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * | Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $server
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
    [pscustomobject]$output
}

Listing 14-3: 将你的output对象放入循环中并设置服务器名称

请注意,你在填充数据后才将 output 创建为哈希表并将其转换为 PSCustomObject。之所以这样做,是因为将属性值保存在哈希表中比保存在 PSCustomObject 中更简单;你只有在输出时才关心 output 是该类型的对象,以便当你引入其他信息源时,它们都将是相同的对象类型。

你可以通过以下代码查看你的 PSCustomObject 所有属性的名称,以及你正在查询的服务器名称:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ --------------- --------------- --------- -----------
SQLSRV1
WEBSRV1

如你所见,你已经有了数据。它可能看起来不多,但你已经走在了正确的道路上!

查询远程文件

现在你已经知道如何存储数据,接下来只需要获取数据。这意味着需要从每个服务器中提取所需的信息,并仅返回你关心的属性。让我们从 UserProfileSize(MB)的值开始。为此,让我们想办法找出每个服务器的 C:\Users 文件夹中所有这些配置文件占用了多少空间。

由于你设置了循环的方式,你需要弄清楚如何仅为一个服务器执行此操作。既然你知道文件夹路径是 C:\Users,那么让我们先看看你是否能查询到所有服务器的用户配置文件夹下的所有文件。

当你运行 Get-ChildItem -Path \\WEBSRV1\c$\Users -Recurse -File 并且有权限访问该文件共享时,你会看到它返回了所有用户配置文件中的所有文件和文件夹,但没有看到任何与大小相关的信息。让我们将输出通过管道传递给 Select-Object,以返回所有属性:

PS> Get-ChildItem -Path \\WEBSRV1\c$\Users -Recurse -File | Select-Object -Property *

PSPath            : Microsoft.PowerShell.Core\FileSystem::\WEBSRV1\c$\Users\Adam\file.log
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::\\WEBSRV1\c$\Users\Adam
PSChildName       : file.log
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : False
Mode              : -a----
VersionInfo       : File:             \\WEBSRV1\c$\Users\Adam\file.log
                    InternalName:
                    OriginalFilename:
                    FileVersion:
                    FileDescription:
                    Product:
                    ProductVersion:
                    Debug:            False
                    Patched:          False
                    PreRelease:       False
                    PrivateBuild:     False
                    SpecialBuild:     False
                    Language:
BaseName          : file
Target            :
LinkType          :
Name              : file.log
Length            : 8926
DirectoryName     : \\WEBSRV1\c$\Users\Adam
--snip--

Length 属性显示文件的大小(以字节为单位)。知道这一点后,你需要计算服务器 C:\Users 文件夹中每个文件的 Length 值的总和。幸运的是,PowerShell 通过其中一个 cmdlet Measure-Object 使这一过程变得简单。这个 cmdlet 接受来自管道的输入,并自动将特定属性的值加总起来:

PS> Get-ChildItem -Path '\\WEBSRV1\c$\Users\' -File -Recurse | Measure-Object -Property Length -Sum

Count    : 15
Average  :
Sum      : 600554
Maximum  :
Minimum  :
Property : Length

现在你有了一个属性(Sum),可以用来表示输出中总的用户配置文件大小。此时,只需将代码整合到循环中,并在 $output 哈希表中设置适当的属性。由于你只需要从 Measure-Object 返回的对象中获取 Sum 属性,因此你会将命令括在括号中,并像 清单 14-4 中那样引用 Sum 属性。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * | Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfileSize (MB)'        = $null
        'StoppedServices'             = $null
    }
    $output.ServerName = $server
    $output.'UserProfileSize (MB)' = (Get-ChildItem -Path '\\WEBSRV1\c$\Users\' -File -Recurse |
    Measure-Object -Property Length -Sum).Sum
    [pscustomobject]$output
}

清单 14-4:更新脚本以存储 UserProfilesSize

如果你运行该脚本,结果如下:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ --------------- --------------- --------- -----------
SQLSRV1                   636245
WEBSRV1                   600554

如你所见,你现在得到了用户配置文件的总大小——但它还不是以兆字节为单位。你计算了 Length 的总和,而 Length 是以字节为单位的。PowerShell 使得这种转换变得简单:只需将数字除以 1MB,就可以得到结果。你可能会看到结果以小数点形式表示。你可以采取最后一步,将输出转换为整数,以确保你得到的是一个整数,这样就可以将数字“四舍五入”到一个完整的兆字节值:

$userProfileSize = (Get-ChildItem -Path "\\$server\c$\Users\" -File |
Measure-Object -Property Length -Sum).Sum
$output.'UserProfilesSize (MB)' = int

查询 Windows 管理工具

你还有五个值需要填充。对于其中四个,你将使用一个名为Windows 管理工具(WMI)的微软内置功能。WMI 基于行业标准的通用信息模型(CIM),是一个包含与操作系统及其运行硬件相关的数千个属性的实时信息库。这些信息被分隔成不同的命名空间、类和属性。如果你正在寻找有关计算机的信息,你很可能会经常使用 WMI。

对于这个特定的脚本,你将提取硬盘空间、操作系统版本、服务器的 IP 地址,以及服务器包含的内存量的信息。

PowerShell 有两个命令用于查询 WMI:Get-WmiObjectGet-CimInstanceGet-WmiObject是较旧的命令,灵活性不如Get-CimInstance(如果你想了解技术细节:这主要是因为Get-WmiObject只使用 DCOM 来连接远程计算机,而Get-CimInstance默认使用 WSMAN,也可以选择使用 DCOM)。目前,微软似乎将所有精力都投入到Get-CimInstance中,所以你将使用这个命令。关于 CIM 与 WMI 的详细对比,可以参考这篇博客:https://blogs.technet.microsoft.com/heyscriptingguy/2016/02/08/should-i-use-cim-or-wmi-with-windows-powershell/

查询 WMI 的最难部分是弄清楚你想要的信息藏在哪里。通常,你需要自己做这个研究(我鼓励你在这里尝试),但为了节省时间,让我为你提供这个脚本的答案:所有存储资源使用情况都在Win32_LogicalDisk中,操作系统的信息在Win32_OperatingSystem中,Windows 服务都在Win32_Service中,任何网络适配器的信息都在Win32_NetworkAdapterConfiguration中,内存信息则在Win32_PhysicalMemory中。

现在让我们看看如何使用Get-CimInstance查询这些 WMI 类,获取你需要的属性。

磁盘剩余空间

我们从可用的硬盘空间开始,这些信息存储在Win32_LogicalDisk中。像处理UserProfilesSize一样,你将从一台服务器开始,然后在循环中进行泛化。在这里,你很幸运;你甚至不需要使用Select-Object来挖掘所有的属性——FreeSpace就在这里:

PS> Get-CimInstance -ComputerName sqlsrv1 -ClassName Win32_LogicalDisk

DeviceID DriveType ProviderName VolumeName Size        FreeSpace   PSComputerName
-------- --------- ------------ ---------- ----        ---------   --------------
C:       3                                 42708496384 34145906688 sqlsrv1

了解到Get-CimInstance返回的是一个对象后,你只需访问所需的属性,就能获取到剩余空间的数值:

PS> (Get-CimInstance -ComputerName sqlsrv1 -ClassName Win32_LogicalDisk).FreeSpace
34145906688

你已经获得了数值,但是像上次一样,它是以字节为单位的(这是 WMI 中的常见情况)。你可以像之前一样进行转换,只不过这次你需要的是千兆字节,所以你要将其除以1GB。当你更新脚本,通过将FreeSpace属性除以1GB时,输出结果大概是这样:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ --------------- --------------- --------- -----------
SQLSRV1                   636245          31.800853729248
WEBSRV1                   603942         34.5973815917969

你不需要看到 12 位数字的空闲空间,因此可以通过使用[Math]类的Round()方法进行四舍五入,使输出看起来更好:

$output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance -ComputerName $server
-ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ --------------- --------------- --------- -----------
SQLSRV1                   636245                     31.8
WEBSRV1                   603942                     34.6

现在这些值更容易阅读了。三个已经完成,剩下四个。

操作系统信息

到现在你应该能看到一般的模式:查询单台服务器,找到合适的属性,然后将查询添加到你的foreach循环中。

从现在开始,你只需在foreach循环中添加行。缩小类、类属性和属性值的过程对于你从 WMI 查询的任何值都是一样的。只需遵循相同的一般模式:

$output.'PropertyName' = (Get-CimInstance -ComputerName ServerName 
-ClassName WMIClassName).WMIClassPropertyName

添加下一个值后,你的脚本看起来像示例 14-5。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
    $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum / 1MB
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    -ComputerName $server -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance -ComputerName $server
    -ClassName Win32_OperatingSystem).Caption
    [pscustomobject]$output
}

示例 14-5:更新后的脚本,包含OperatingSystem查询

现在运行你的脚本:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem                           StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ ---------------                           --------------- --------- -----------
SQLSRV1                   636245         31.8005790710449 Microsoft Windows Server 2016 Standard
WEBSRV1                   603942         34.5973815917969 Microsoft Windows Server 2012 R2 Standard

你已经获得了一些有用的操作系统信息。让我们迈出下一步,看看如何查询内存信息。

内存

接下来是收集下一条信息(Memory),你将使用Win32_PhysicalMemory类。再次在单台服务器上测试你的查询,能够获得你需要的信息。在这种情况下,你需要的内存信息存储在Capacity中:

PS> Get-CimInstance -ComputerName sqlsrv1 -ClassName Win32_PhysicalMemory

Caption              : Physical Memory
Description          : Physical Memory
InstallDate          :
Name                 : Physical Memory
Status               :
CreationClassName    : Win32_PhysicalMemory
Manufacturer         : Microsoft Corporation
Model                :
OtherIdentifyingInfo :
--snip--
Capacity             : 2147483648
--snip--

Win32_PhysicalMemory下的每个实例代表一个内存条。你可以把内存条看作服务器中的一根物理内存条。恰好我的 SQLSRV1 服务器只有一根内存条。然而,你肯定会找到有更多内存条的服务器。

由于你需要查询服务器的总内存,你必须按照获取配置文件大小时使用的相同步骤进行操作。你需要将所有实例中的Capacity值加起来。幸运的是,Measure-Object cmdlet 可以跨任何数量的对象类型工作。只要属性是数字,它就能将它们加起来。

同样,由于Capacity是以字节表示的,你需要将其转换为适当的标签:

PS> (Get-CimInstance -ComputerName sqlsrv1 -ClassName Win32_PhysicalMemory |
Measure-Object -Property Capacity -Sum).Sum /1GB
2

正如你在示例 14-6 中看到的,你的脚本越来越长!

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * | Select-Object
-ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
 $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum / 1MB
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    -ComputerName $server -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance -ComputerName $server
    -ClassName Win32_OperatingSystem).Caption
    $output.'Memory (GB)' = (Get-CimInstance -ComputerName $server -ClassName
    Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB
    [pscustomobject]$output
}

示例 14-6:包含Memory查询的脚本

让我们看看到目前为止的输出:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem                           StoppedServices IPAddress Memory (GB)
---------- --------------------- ------------------------ ---------------                           --------------- --------- -----------
SQLSRV1                   636245                     31.8 Microsoft Windows Server 2016 Standard                                        2
WEBSRV1                   603942                     34.6 Microsoft Windows Server 2012 R2 Standard                                     2

到此为止,你只剩下两个字段需要填写!

网络信息

最后一项 WMI 信息是 IP 地址,它来自Win32_NetworkAdapterConfiguration。我把找 IP 地址的任务放到最后,因为与其他数据条目不同,找到服务器的 IP 地址不像找到一个值然后将其添加到$output哈希表那样简单。你需要做一些筛选操作来缩小范围。

让我们首先看看使用到目前为止的方法输出是什么样的:

PS> Get-CimInstance -ComputerName SQLSRV1 -ClassName Win32_NetworkAdapterConfiguration

ServiceName    DHCPEnabled    Index    Description   PSComputerName
-----------    -----------    -----    -----------   --------------
kdnic          True           0        Microsoft...  SQLSRV1
netvsc         False          1        Microsoft...  SQLSRV1
tunnel         False          2        Microsoft...  SQLSRV1

你会立刻发现默认输出不显示 IP 地址,不过这并没有阻止你。但是,更棘手的是,这个命令并没有返回一个实例。这个服务器上有三个网络适配器。你如何选择包含你要查找的 IP 地址的那个呢?

首先,你需要通过使用 Select-Object 查看所有属性。使用 Get-CimInstance -ComputerName SQLSRV1 -ClassName Win32_NetworkAdapterConfiguration | Select-Object -Property *,你可以浏览(大量的)输出。根据服务器上安装的网络适配器,你可能会注意到某些字段在 IPAddress 属性上没有任何内容。这是很常见的,因为某些网络适配器没有 IP 地址。然而,当你找到绑定有 IP 地址的适配器时,它应该类似于以下代码,你可以看到 IPAddress 属性 ❶ 在这个例子中有一个 IPv4 地址 192.168.0.40 和几个 IPv6 地址:

   DHCPLeaseExpires             :
   Index                        : 1
   Description                  : Microsoft Hyper-V Network Adapter
   DHCPEnabled                  : False
   DHCPLeaseObtained            :
   DHCPServer                   :
   DNSDomain                    : Powerlab.local
   DNSDomainSuffixSearchOrder   : {Powerlab.local}
   DNSEnabledForWINSResolution  : False
   DNSHostName                  : SQLSRV1
   DNSServerSearchOrder         : {192.168.0.100}
   DomainDNSRegistrationEnabled : True
   FullDNSRegistrationEnabled   : True
❶ IPAddress                     : {192.168.0.40... 
   IPConnectionMetric           : 20
   IPEnabled                    : True
   IPFilterSecurityEnabled      : False
   --snip--

这个脚本需要动态并支持多种网络适配器配置。确保脚本能够处理除你正在使用的 Microsoft Hyper-V Network Adapter 之外的其他类型的网络适配器非常重要。你需要找到一个标准的过滤标准,这样它就能适用于所有服务器。

IPEnabled 属性是关键。当这个属性设置为 True 时,TCP/IP 协议已绑定到这个网络适配器,这是拥有 IP 地址的前提。如果你能缩小到那个 IPEnabled 属性设置为 True 的网卡,那么你就找到了你需要的适配器。

在过滤 WMI 实例时,最好使用 Get-CimInstance 上的 Filter 参数。PowerShell 社区有句名言:filter left。基本意思是,如果可以的话,总是尽可能早地过滤输出——也就是说,尽早过滤,这样你就不会把不必要的对象送入管道。除非必须,否则不要使用 Where-Object。如果管道中没有不需要的对象,性能会更快。

Get-CimInstance 上的 Filter 参数使用的是 Windows 查询语言(WQL),这是 结构化查询语言(SQL) 的一个子集。Filter 参数接受与 WQL 相同的 WHERE 子句语法。举个例子:如果在 WQL 中,你希望所有 Win32_NetworkAdapterConfiguration 类实例的 IPEnabled 属性设置为 True,你可以使用 SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = 'True'。由于你已经在 Get-CimInstance 中为 ClassName 参数指定了类名,你需要为 Filter 指定 IPEnabled = 'True'

Get-CimInstance -ComputerName SQLSRV1 -ClassName Win32_NetworkAdapterConfiguration
-Filter "IPEnabled = 'True'" | Select-Object -Property *

这应该只返回那些 IPEnabled(意味着它们有 IP 地址)的网络适配器。

现在你已经有了一个单一的 WMI 实例,并且知道你要找的属性是 IPAddress,我们来看看在查询单个服务器时它是什么样子的。你将使用你一直在使用的 object.property 语法:

PS> (Get-CimInstance -ComputerName SQLSRV1 -ClassName Win32_NetworkAdapterConfiguration
-Filter "IPEnabled = 'True'").IPAddress

192.168.0.40
fe80::e4e1:c511:e38b:4f05
2607:fcc8:acd9:1f00:e4e1:c511:e38b:4f05

哎呀!看起来里面有 IPv4 和 IPv6 的引用。你需要过滤更多的元素。由于 WQL 无法对属性值进行更深层次的过滤,你需要解析出 IPv4 地址。

经过一些调查,你可以看到所有地址都被花括号包围,并且由逗号分隔:

IPAddress : {192.168.0.40, fe80::e4e1:c511:e38b:4f05, 2607:fcc8:acd9:1f00:e4e1:c511:e38b:4f05}

这表明该属性不是作为一个大字符串存储,而是作为一个数组。为了确认它是一个数组,你可以尝试使用索引来查看是否能只获取 IPv4 地址:

PS> (Get-CimInstance -ComputerName SQLSRV1 -ClassName Win32_NetworkAdapterConfiguration
-Filter "IPEnabled = 'True'").IPAddress[0]

192.168.0.40

你真幸运!IPAddress属性确实是一个数组。此时,你已经得到了值,可以将完整的命令添加到脚本中,如列表 14-7 所示。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
 'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
    $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum / 1MB
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    -ComputerName $server -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance -ComputerName $server
    -ClassName Win32_OperatingSystem).Caption
    $output.'Memory (GB)' = (Get-CimInstance -ComputerName $server -ClassName
    Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB
    $output.'IPAddress' = (Get-CimInstance -ComputerName $server -ClassName
    Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'").IPAddress[0]
    [pscustomobject]$output
}

列表 14-7:更新后的代码,现在可以处理IPAddress

现在你运行这个:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem                          StoppedServices IPAddress     Memory (GB)
---------- --------------------- ------------------------ ---------------                          --------------- ---------     -----------
SQLSRV1                   636245                     31.8 Microsoft Windows Server 2016 Standard                   192.168.0.40  2
WEBSRV1                   603942                     34.6 Microsoft Windows Server 2012 R2 Standard                192.168.0.70  2

现在你已经收集到所有所需的 WMI 信息,只剩下最后一件事了。

Windows 服务

收集的最后一项数据是服务器上已停止的服务列表。你将按照我们的基本算法,首先在单台服务器上进行测试。为此,你将使用Get-Service命令在服务器上运行,这将返回所有正在使用的服务。然后,你将把输出通过管道传递给Where-Object命令,仅筛选出状态为Stopped的服务。总的来说,命令将如下所示:Get-Service -ComputerName sqlsrv1 | Where-Object { $_.Status -eq 'Stopped' }

这个命令返回的是包含所有属性的完整对象。但你只是想要服务名称,所以你将使用你一直在使用的技巧——引用属性名称——并只返回服务名称的列表。

PS> (Get-Service -ComputerName sqlsrv1 | Where-Object { $_.Status -eq 'Stopped' }).DisplayName
Application Identity
Application Management
AppX Deployment Service (AppXSVC)
--snip--

将这一部分添加到你的脚本中,你将得到列表 14-8。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
    $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum / 1MB
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    -ComputerName $server -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance -ComputerName $server
    -ClassName Win32_OperatingSystem).Caption
    $output.'Memory (GB)' = (Get-CimInstance -ComputerName $server -ClassName
    Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB
    $output.'IPAddress' = (Get-CimInstance -ComputerName $server -ClassName
    Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'").IPAddress[0]
    $output.StoppedServices = (Get-Service -ComputerName $server |
    Where-Object { $_.Status -eq 'Stopped' }).DisplayName
    [pscustomobject]$output
}

列表 14-8:更新并使用你的脚本打印已停止的服务

运行以下代码来测试你的脚本:

PS> C:\Get-ServerInformation.ps1 | Format-Table -AutoSize

ServerName UserProfilesSize (MB) AvailableDriveSpace (GB) OperatingSystem                           StoppedServices
---------- --------------------- ------------------------ ---------------                           ---------------
SQLSRV1                   636245                     31.8 Microsoft Windows Server 2016 Standard    {Application Identity,
                                                                                                    Application Management,
                                                                                                    AppX Deployment Servi...
WEBSRV1                   603942                     34.6 Microsoft Windows Server 2012 R2 Standard {Application Experience,
                                                                                                    Application Management,
                                                                                                    Background Intellig...

关于已停止的服务,一切看起来都正常——但是其他的属性去哪儿了?此时,控制台窗口已经没有空间了。移除Format-Table引用可以让你看到所有的值:

PS> C:\Get-ServerInformation.ps1 

ServerName               : SQLSRV1
UserProfilesSize (MB)    : 636245
AvailableDriveSpace (GB) : 31.8
OperatingSystem          : Microsoft Windows Server 2016 Standard
StoppedServices          : {Application Identity, Applic... 
IPAddress                : 192.168.0.40
Memory (GB)              : 2

ServerName               : WEBSRV1
UserProfilesSize (MB)    : 603942
AvailableDriveSpace (GB) : 34.6
OperatingSystem          : Microsoft Windows Server 2012 R2 Standard
StoppedServices          : {Application Experience, Application Management, 
                           Background Intelligent Transfer Service, Computer 
                           Browser...}
IPAddress                : 192.168.0.70
Memory (GB)              : 2

看起来不错!

脚本清理与优化

在宣布胜利并继续之前,让我们稍微反思一下。编写代码是一个迭代过程。你完全有可能一开始设定了目标,达成了目标,但最终还是写出了糟糕的代码——优秀的程序不仅仅是完成需要做的事。脚本现在确实完成了你想要的功能,但你可以用更好的方式来实现。如何做呢?

回想一下 DRY 原则:不要重复自己。你可以看到这个脚本中有很多重复的地方。你有许多Get-CimInstance的引用,里面使用了相同的参数。你还在为同一个服务器多次调用 WMI。这些地方看起来是让代码更高效的好机会。

首先,CIM cmdlets 有一个CimSession参数。这个参数允许你创建一个 CIM 会话并在之后重用它。与其创建一个临时会话,使用它,然后销毁它,不如创建一个会话,随时使用,然后销毁它,正如 Listing 14-9 中所示。这个概念类似于我们在第八章中介绍的Invoke-Command命令的Session参数。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
foreach ($server in $servers) {
 $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
    }
    $cimSession = New-CimSession -ComputerName $server
    $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    -CimSession $cimSession -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance -CimSession $cimSession
    -ClassName Win32_OperatingSystem).Caption
    $output.'Memory (GB)' = (Get-CimInstance -CimSession $cimSession
    -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum)
    .Sum /1GB
    $output.'IPAddress' = (Get-CimInstance -CimSession $cimSession -ClassName
    Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'").IPAddress[0]
    $output.StoppedServices = (Get-Service -ComputerName $server |
    Where-Object { $_.Status -eq 'Stopped' }).DisplayName
    Remove-CimSession -CimSession $cimSession
    [pscustomobject]$output
}

Listing 14-9: 更新你的代码以创建并重用单个会话

现在你正在重用单个 CIM 会话,而不是多个会话。但你仍然在不同命令的参数中多次引用它。为了更好地优化,你可以创建一个哈希表,并为其分配一个名为CIMSession的键,将你刚创建的 CIM 会话作为值。一旦你在哈希表中保存了一个通用的参数集,就可以在所有Get-CimInstance引用中重用它。

这种技巧被称为splatting,你可以通过在调用每个Get-CimInstance引用时,使用@符号后跟哈希表名称来实现,如 Listing 14-10 所示。

Get-ServerInformation.ps1
-------------------
$serversOuPath = 'OU=Servers,DC=powerlab,DC=local'
$servers = Get-ADComputer -SearchBase $serversOuPath -Filter * |
Select-Object -ExpandProperty Name
foreach ($server in $servers) {
    $output = @{
        'ServerName'                  = $null
        'IPAddress'                   = $null
        'OperatingSystem'             = $null
        'AvailableDriveSpace (GB)'    = $null
        'Memory (GB)'                 = $null
        'UserProfilesSize (MB)'       = $null
        'StoppedServices'             = $null
 }
    $getCimInstParams = @{
        CimSession = New-CimSession -ComputerName $server
    }
    $output.ServerName = $server
    $output.'UserProfilesSize (MB)' = (Get-ChildItem -Path "\\$server\c$\
    Users\" -File | Measure-Object -Property Length -Sum).Sum
    $output.'AvailableDriveSpace (GB)' = [Math]::Round(((Get-CimInstance
    @getCimInstParams -ClassName Win32_LogicalDisk).FreeSpace / 1GB),1)
    $output.'OperatingSystem' = (Get-CimInstance @getCimInstParams -ClassName
    Win32_OperatingSystem).Caption
    $output.'Memory (GB)' = (Get-CimInstance @getCimInstParams -ClassName
    Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB
    $output.'IPAddress' = (Get-CimInstance @getCimInstParams -ClassName
    Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'").IPAddress[0]
    $output.StoppedServices = (Get-Service -ComputerName $server |
    Where-Object { $_.Status -eq 'Stopped' }).DisplayName
    Remove-CimSession -CimSession $getCimInstParams.CimSession
    [pscustomobject]$output
}

Listing 14-10: 创建CIMSession参数以供重用

到此为止,你可能已经习惯于以dash<参数名称> <参数值>的格式将参数传递给命令。这种方式有效,但如果你反复将相同的参数传递给命令,它会变得低效。相反,你可以像这里一样使用 splatting,通过创建一个哈希表,然后将该哈希表传递给每个需要相同参数的命令。

现在你已经完全删除了$cimSession变量。

总结

在本章中,你从所有前面的章节中提取了关键信息,并将其应用到你在现实世界中可能遇到的情境中。我通常推荐创建的一种脚本类型是查询信息的脚本。它教会你很多 PowerShell 的知识,而且出错的几率很小!

你在本章中进行了逐步迭代,从目标到解决方案,再到更好的解决方案。这是你在使用 PowerShell 时将反复遵循的过程。定义目标,从小处着手,搭建框架(在本例中是foreach循环),然后逐步添加代码,一步步克服障碍,直到一切都达成。

一旦你完成了脚本,记住,你实际上并未完全完成,直到你回顾你的代码:看看如何使它更高效、使用更少的资源并提升速度。经验会让优化变得更容易。你将建立起必要的视角,直到优化成为一种自然而然的行为。当你完成优化后,坐下来,享受成功的喜悦,准备好开始下一个项目吧!

第三部分

构建你自己的模块

到目前为止,你应该已经对 PowerShell 的特点有了清晰的了解。我们已经讨论了语言的语法,以及一些你在日常自动化工作中可能会使用的特定模块。但直到前一章为止,我们所做的事情都是零散的:这里一点语法,那里一点语法,没有什么重要的内容。在第十四章中,使用服务器库存脚本,你第一次尝试了进行一个较长时间的 PowerShell 项目。在第三部分中,我们将做得更大:你将构建自己的 PowerShell 模块。

PowerLab

PowerLab 是一个单独的 PowerShell 模块,包含了你从零开始配置 Windows 服务器所需的功能。你将一步一步构建 PowerLab;如果你想看到最终结果,可以在这个 GitHub 仓库中找到:github.com/adbertram/PowerLab

从零开始配置 Windows 服务器的过程大致如下:

  • 创建虚拟机。

  • 安装 Windows 操作系统。

  • 安装服务器服务(Active Directory、SQL Server 或 IIS)。

这意味着你需要 PowerLab 模块来完成五个任务:

  • 创建 Hyper-V 虚拟机

  • 安装 Windows 服务器

  • 创建 Active Directory 林

  • 配置 SQL 服务器

  • 配置 IIS Web 服务器

为了完成这些任务,你将使用三个主要命令:

  • New-PowerLabActiveDirectoryForest

  • New-PowerLabSqlServer

  • New-PowerLabWebServer

当然,你会使用超过三个命令。你将通过多个辅助命令构建每个命令,这些辅助命令将处理后台功能,包括创建虚拟机和安装操作系统。但我们将在接下来的章节中详细介绍这一切。

先决条件

构建 PowerLab 你需要一些东西:

  • 一台 Windows 10 专业版客户端计算机,位于工作组中。已加入域的 Windows 10 计算机可能也能使用,但未经过测试。

  • 一台运行 Windows Server 2012 R2(至少)并与客户端处于同一网络中的 Hyper-V 主机—尽管主机也可以加入域,但此场景未经过测试。

  • 位于 Hyper-V 主机上的 Windows Server 2016 的 ISO 文件。未测试 Windows Server 2019。你可以从 https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2016?filetype=ISO 下载 Windows Server 的评估版本。

  • 在客户端计算机上启用远程服务器管理工具(RSAT)(从 https://www.microsoft.com/en-us/download/details.aspx?id=45520 下载)。

  • 在你的客户端计算机上安装最新版本的 Pester PowerShell 模块。

你还需要以本地管理员组成员身份登录客户端计算机,并将 PowerShell 执行策略设置为 unrestricted。(你可以运行 Set-ExecutionPolicy Unrestricted 来更改执行策略,但我建议在实验室设置完成后将其更改回 AllSignedRemoteSigned。)

设置 PowerLab

当像 PowerLab 这样的工具提供给消费者时,你希望让设置过程尽可能无痛。一种方法是提供一个脚本,处理模块的安装和配置,并且需要尽量少的用户输入。

我已经编写好了 PowerLab 的安装脚本。它可以在 PowerLab 的 GitHub 仓库中找到:https://raw.githubusercontent.com/adbertram/PowerLab/master/Install-PowerLab.ps1。该链接将提供脚本的原始源代码。你可以将其复制并粘贴到一个新的文本文件中,并保存为 Install-PowerLab.ps1,不过这是一本 PowerShell 书籍,因此我们可以尝试运行以下命令:

PS> Invoke-WebRequest -Uri 'http://bit.ly/powerlabinstaller' -OutFile 'C:\Install-PowerLab.ps1'

请注意:当你运行脚本时,你需要回答一些问题。你需要提供 Hyper-V 主机的主机名、Hyper-V 主机的 IP 地址、Hyper-V 主机的本地管理员用户名和密码,以及每个要安装的操作系统的产品密钥(如果没有使用 Windows Server 评估版)。

一旦你拥有所有必要的信息,就可以使用以下命令运行安装脚本:

PS> C:\Install-PowerLab.ps1

Name of your HYPERV host: HYPERVSRV
IP address of your HYPERV host: 192.168.0.200
Enabling PS remoting on local computer...
Adding server to trusted computers...
PS remoting is already enabled on [HYPERVSRV]
Setting firewall rules on Hyper-V host...
Adding the ANONYMOUS LOGON user to the local machine and host server
Distributed COM Users group for Hyper-V manager
Enabling applicable firewall rules on local machine...
Adding saved credential on local computer for Hyper-V host...
Ensure all values in the PowerLab configuration file are valid and close the
ISE when complete.
Enabling the Microsoft-Hyper-V-Tools-All features...
Lab setup is now complete.

如果你想检查这个脚本的具体内容,可以通过书籍的资源下载并查看它。不过,请知道这个脚本是为了让我们都能到达相同的基础架构,而不一定是为了展示脚本的具体操作;此时它可能超出了你的理解范围。这个脚本的目的是帮助你跟上我的进度。

演示代码

你在接下来的章节中编写的所有代码都可以在 github.com/adbertram/PowerShellForSysadmins/tree/master/Part%20III 找到。除了所有的 PowerLab 代码,你还会找到必要的数据文件以及 Pester 脚本,用于测试模块并验证你的环境是否满足所有预期的前提条件。在开始每一章之前,我强烈建议你使用 Invoke-Pester 命令来运行每章文件中找到的 Prerequisites.Tests.ps1 Pester 脚本。这样做可以避免后续出现许多令人头痛的 bug。

摘要

你应该已经准备好开始构建 PowerLab。接下来的章节将涉及很多内容,并且会涉及 PowerShell 的多个领域,所以如果你看到不认识的东西不要感到惊讶。网络上有很多资源可以帮助你解决复杂的语法问题,如果你不理解某些内容,也可以随时在 Twitter 上联系我,用户名是 @adbertram,或者向互联网中的其他人求助。

既然如此,让我们开始吧!

第十五章:配置虚拟环境

图片

PowerLab 是一个最终的大型项目,涵盖了你所学的所有概念以及更多内容。它是一个自动化 Hyper-V 虚拟机(VM)配置的项目,包括安装和配置服务,如 SQL 和 IIS。试想一下,只需运行一个命令,如 New-PowerLabSqlServerNew-PowerLabIISServer,甚至是 New-PowerLab,等待几分钟,就能获得一个完全配置好的机器(或多台机器)。如果你跟着我完成本书的剩余部分,这就是你将得到的成果。

PowerLab 项目的目的是消除创建测试环境或实验室时所需的所有重复、耗时的任务。完成后,你只需少数几个命令就能从一个 Hyper-V 主机和几个 ISO 文件构建一个完整的 Active Directory 林。

我故意没有在第一部分和第二部分中涵盖 PowerLab 中的所有内容。相反,我挑战你注意这些领域并自行想出独特的解决方案。毕竟,在编程中,总是有很多方法可以完成同一任务。如果你遇到困难,请随时通过 Twitter @adbertram 联系我。

通过构建一个如此规模的项目,你不仅可以覆盖数百个 PowerShell 主题,还能看到脚本语言的强大功能,并获得一个显著节省时间的工具。

在本章中,你将通过创建基础的 PowerLab 模块来启动 PowerLab。然后,你将添加自动化创建虚拟交换机、虚拟机和虚拟硬盘(VHD)的功能。

PowerLab 模块先决条件

为了跟上第三部分中所有的代码示例,你需要满足一些先决条件。每一章都会有一个“先决条件”部分,确保你始终知道该期待什么。

本章的项目需要一个配置如下的 Hyper-V 主机:

  • 一个网络适配器

  • IP: 10.0.0.5(可选,但为了完全按照示例进行,你需要此 IP)

  • 子网掩码:255.255.255.0

  • 一个工作组

  • 至少 100GB 的可用存储

  • 带有完整图形用户界面的 Windows Server 2016

要创建一个 Hyper-V 服务器,你需要在计划使用的 Windows 服务器上安装 Hyper-V 角色。你可以通过下载并运行书中资源中的 Hyper-V Setup.ps1 脚本来加快设置过程,网址为 github.com/adbertram/PowerShellForSysadmins/。这将设置 Hyper-V 并创建一些必要的文件夹。

注意

如果你打算逐字跟随,请运行关联章节的 Pester 前提脚本 (Prerequisites.Tests.ps1) 以确认你的 Hyper-V 服务器已按预期设置。这些测试将确认你的实验环境与我的设置完全一致。运行 Invoke-Pester,并传递前提脚本,像 列表 15-1 中那样。书中的其余代码将直接在 Hyper-V 主机上执行。

PS> Invoke-Pester -Path 'C:\PowerShellForSysadmins\Part III\Automating Hyper-V\Prerequisites
.Tests.ps1'

Describing Automating Hyper-V Chapter Prerequisites
 [+] Hyper-V host server should have the Hyper-V Windows feature installed 2.23s
 [+] Hyper-V host server is Windows Server 2016 147ms
 [+] Hyper-V host server should have at least 100GB of available storage 96ms
 [+] has a PowerLab folder at the root of C 130ms
 [+] has a PowerLab\VMs folder at the root of C 41ms
 [+] has a PowerLab\VHDs folder at the root of C 47ms
Tests completed in 2.69s
Passed: 5 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0

列表 15-1:运行 Pester 前提检查以确保 Hyper-V 工作正常

如果你成功设置了环境,输出应该会确认五个测试通过。确认环境已准备好后,你可以开始项目!

创建模块

因为你知道自己将需要自动化多个彼此相关的任务,所以你应该创建一个 PowerShell 模块。正如你在 第七章 中所学,PowerShell 模块是将多个相似功能合并为一个单元的好方法;这样,你可以轻松管理执行特定任务所需的所有代码。PowerLab 也不例外。没有必要一次性考虑所有内容,所以从小处着手——添加功能,测试,并重复。

创建空模块

首先,你需要创建一个空模块。为此,请远程桌面连接到即将成为 Hyper-V 主机的计算机,并以本地管理员身份登录——或以任何本地管理员组的帐户登录。你将直接在 Hyper-V 主机上构建这个模块,以便简化虚拟机的创建和管理。这意味着你将使用 RDP 会话连接到 Hyper-V 主机的控制台会话。然后,你将创建模块文件夹、模块本身(.psm1 文件)和可选的清单(.psd1 文件)。

由于你是通过本地管理员帐户登录,并且将来可能允许其他人使用你的 PowerLab 模块,建议将模块创建在 C:\ProgramFiles\WindowsPowerShell\Modules 目录下。这样,无论何时作为任何管理员用户登录主机,你都可以访问该模块。

接下来,打开 PowerShell 控制台并选择 以管理员身份运行。然后,使用以下命令创建 PowerLab 模块文件夹:

PS> New-Item -Path C:\Program Files\WindowsPowerShell\Modules\PowerLab -ItemType Directory

接下来,创建一个名为 PowerLab.psm1 的空文本文件。使用 New-Item 命令:

PS> New-Item -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\PowerLab.psm1'

创建模块清单

现在,创建一个模块清单。要创建模块清单,使用便捷的 New-ModuleManifest 命令。此命令创建一个模板清单,你可以在初始文件构建后用文本编辑器打开并根据需要进行调整。以下是我用来构建模板清单的参数:

PS> New-ModuleManifest -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\PowerLab.psd1' 
-Author 'Adam Bertram' 
-CompanyName 'Adam the Automator, LLC' 
-RootModule 'PowerLab.psm1' 
-Description 'This module automates all tasks to provision entire environments of a domain
controller, SQL server and IIS web server from scratch.'

随意修改参数值以满足你的需求。

使用内置前缀命名函数

函数不一定需要特定的名称。然而,当你构建一个通常由相关函数组成的模块时,最好在函数名的名词部分前加上相同的标签。例如,你的项目名为PowerLab。在这个项目中,你将构建与该共同主题相关的函数。为了将 PowerLab 中的函数与其他模块中的函数区分开来,你可以在函数名的实际名词部分前加上模块名。这意味着大多数函数的名词将以PowerLab为开头。

然而,并不是所有的函数都将以模块名开头。例如,一些仅协助其他函数且永远不会被用户调用的辅助函数。

当你确定要让所有函数名的名词都使用相同的前缀,而不必在函数名中明确指定时,模块清单中有一个选项叫做DefaultCommandPrefix。这个选项将强制 PowerShell 在名词前加上特定的字符串。例如,如果你在清单中定义了DefaultCommandPrefix键,并在模块中创建了一个名为New-Switch的函数,那么当模块被导入时,这个函数将无法作为New-Switch使用,而是作为New-PowerLabSwitch

# Default prefix for commands exported from this modul...
# DefaultCommandPrefix = ''

我倾向于采用这种方式,因为它会强制在模块中的所有函数名的名词部分前加上这个字符串。

导入新模块

现在你已经构建了清单,接下来可以检查它是否成功导入。由于你还没有编写任何函数,模块不会执行任何操作,但重要的是检查 PowerShell 是否能够识别该模块。如果你看到以下结果,那么你就可以继续了。

PS> Get-Module -Name PowerLab –ListAvailable

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     1.0        PowerLab

如果 PowerLab 模块没有出现在输出的底部,请返回前面的步骤检查。此外,确保在 C:\Program Files\WindowsPowerShell\Modules 下已创建 PowerLab 文件夹,并且其中包含 PowerLab.psm1PowerLab.psd1 文件。

自动化虚拟环境的配置

现在你已经构建了模块的结构,可以开始向其中添加功能了。由于创建一个服务器(如 SQL 或 IIS)的任务包含多个相互依赖的步骤,你将首先自动化虚拟交换机、虚拟机和虚拟磁盘的创建。接着你将自动化操作系统部署到这些虚拟机上,最后在这些虚拟机上安装 SQL Server 和 IIS。

虚拟交换机

在你开始自动化创建虚拟机之前,需要确保 Hyper-V 主机上已设置虚拟交换机。虚拟交换机使虚拟机能够与客户端计算机及在同一主机上创建的其他虚拟机进行通信。

手动创建虚拟交换机

你的虚拟交换机将是一个外部交换机,名为PowerLab。这个名字的交换机可能在 Hyper-V 主机上并不存在,但为了确保无误,列出主机上的所有虚拟交换机。你永远不会后悔先检查一遍。

要查看在 Hyper-V 主机上设置的所有交换机,使用 Get-VmSwitch 命令。确认 PowerLab 交换机不存在后,使用 New-VmSwitch 命令创建一个新的虚拟交换机,指定名称(PowerLab)和交换机类型:

PS> New-VMSwitch -Name PowerLab -SwitchType External

由于你需要让虚拟机能够与 Hyper-V 外部的主机通信,因此你将值 External 传递给 SwitchType 参数。无论你与谁分享这个项目,他们也需要创建一个外部交换机。

交换机创建完成后,现在是时候创建 PowerLab 模块的第一个函数了。

自动化虚拟机交换机创建

第一个 PowerLab 功能,称为 New-PowerLabSwitch,用于创建 Hyper-V 交换机。这个功能并不复杂。事实上,如果没有它,你只需要在命令行中执行一个简单的命令——也就是 New-VmSwitch。但是,如果你将这个 Hyper-V 命令包装成一个自定义函数,你将能够执行其他工作:例如,为交换机添加任何类型的默认配置。

我是 幂等性 的忠实粉丝,这个词的意思是“无论命令执行的状态如何,它每次都会执行相同的任务。”在这个例子中,如果创建交换机的任务不是幂等的,那么如果交换机已存在,运行 New-VmSwitch 就会导致错误。

为了去除手动检查交换机是否创建的要求,你可以使用 Get-VmSwitch 命令。这个命令会检查交换机是否已创建。然后,只有当交换机不存在时,你才会尝试创建新的交换机。这使得你可以在任何环境中运行 New-PowerLabSwitch,并且知道它将始终创建虚拟交换机,而不会返回错误——无论 Hyper-V 主机的状态如何。

打开 C:\Program Files\WindowsPowerShell\Modules\PowerLab\PowerLab.psm1 文件并创建 New-PowerLabSwitch 函数,如 Listing 15-2 所示。

function New-PowerLabSwitch {
    param(
        [Parameter()]
        [string]$SwitchName = 'PowerLab',

        [Parameter()]
        [string]$SwitchType = 'External'
    )

    if (-not (Get-VmSwitch -Name $SwitchName -SwitchType $SwitchType -ErrorAction
    SilentlyContinue)) { ❶
        $null = New-VMSwitch -Name $SwitchName -SwitchType $SwitchType ❷
    } else {
        Write-Verbose -Message "The switch [$($SwitchName)] has already been created." ❸
    }
}

Listing 15-2: New-PowerLabSwitch 函数在 PowerLab 模块中的实现

该函数首先检查交换机是否已经创建 ❶。如果没有,函数会创建它 ❷。如果交换机已经创建,函数会向控制台返回一条详细信息 ❸。

保存模块,然后通过使用 Import-Module -Name PowerLab -Force 命令强制重新导入。

当你向模块添加新功能时,必须重新导入模块。如果模块已经导入,你必须使用 Force 参数与 Import-Module 一起强制 PowerShell 重新导入它。否则,PowerShell 会看到模块已经被导入,并跳过它。

一旦你重新导入该模块,New-PowerLabSwitch 函数应该就可以使用了。运行以下命令:

PS> New-PowerLabSwitch –Verbose
VERBOSE: The switch [PowerLab] has already been created.

注意到你没有收到错误信息,而是收到了一个有用的详细信息,说明开关已经创建。这是因为你将可选的Verbose参数传递给了函数。由于SwitchNameSwitchType参数的默认值通常相同,所以这两个参数选择了默认值。

创建虚拟机

现在你已经设置了虚拟交换机,接下来是创建虚拟机。对于这个演示,你将创建一个二代虚拟机,命名为 LABDC,分配 2GB 内存,并连接到你刚刚在 Hyper-V 主机的C:\PowerLab\VMs文件夹中创建的虚拟交换机。我选择LABDC作为名称,因为这将最终成为我们的 Active Directory 域控制器。这个虚拟机最终将成为你完全构建的实验室中的域控制器。

首先,查看所有现有的虚拟机,确保没有同名的虚拟机已经存在。因为你已经知道要创建的虚拟机的名称,所以将该值传递给Get-Vm命令的Name参数:

PS> Get-Vm -Name LABDC
Get-Vm : A parameter is invalid. Hyper-V was unable to find a virtual machine with name LABDC.
At line:1 char:1
+ Get-Vm -Name LABDC
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (LABDC:String) [Get-VM],
                              VirtualizationInvalidArgumentException
    + FullyQualifiedErrorId : InvalidParameter,Microsoft.HyperV.PowerShell.Commands.GetVMCommand

Get-Vm命令找不到指定名称的虚拟机时,它会返回一个错误。由于你只是检查虚拟机是否存在,且此时我们并不关心它是否存在,因此可以使用ErrorAction参数并设置为SilentlyContinue,以确保命令在虚拟机不存在时返回空值。为了简化,这里没有使用try/catch

此技术仅在命令返回的是非终止错误时有效。如果命令返回终止错误,你将需要查看是否能返回所有对象并使用Where-Object进行过滤,或将命令包含在try/catch块中。

手动创建虚拟机

该虚拟机不存在,这意味着你需要创建它。要创建虚拟机,你需要运行Get-Vm命令,并传递在本节开始时定义的值。

PS> New-VM -Name 'LABDC' -Path 'C:\PowerLab\VMs' 
-MemoryStartupBytes 2GB -Switch 'PowerLab' -Generation 2

Name   State CPUUsage(%) MemoryAssigned(M) Uptime   Status             Version
----   ----- ----------- ----------------- ------   ------             -------
LABDC  Off   0           0                 00:00:00 Operating normally 8.0

现在你应该已经有了一台虚拟机,但请通过再次运行 Get-Vm 来确认这一点。

自动化虚拟机创建

要自动化创建一个简单的虚拟机,你需要再添加一个函数。这个函数将遵循与创建新虚拟交换机时相同的模式:编写一个幂等函数,无论 Hyper-V 主机的状态如何,都能执行任务。

New-PowerLabVm函数,如清单 15-3 所示,输入到你的PowerLab.psm1模块中。

function New-PowerLabVm {
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter()]
        [string]$Path = 'C:\PowerLab\VMs',

        [Parameter()]
        [string]$Memory = 4GB,

        [Parameter()]
        [string]$Switch = 'PowerLab',

        [Parameter()]
        [ValidateRange(1, 2)]
        [int]$Generation = 2
    )

 ❶ if (-not (Get-Vm -Name $Name -ErrorAction SilentlyContinue)) {
     ❷ $null = New-VM -Name $Name -Path $Path -MemoryStartupBytes $Memory
        -Switch $Switch -Generation $Generation
    } else {
     ❸ Write-Verbose -Message "The VM [$($Name)] has already been created."
    }
}

清单 15-3:New-PowerLabVm函数,位于PowerLab模块中

该函数检查虚拟机是否已经存在❶。如果不存在,函数将创建一个虚拟机❷。如果已存在,函数将向控制台显示一条详细信息❸。

保存PowerLab.psm1并在命令提示符下执行你刚创建的新函数:

PS> New-PowerLabVm -Name 'LABDC' –Verbose
VERBOSE: The VM [LABDC] has already been created.

再次运行此命令时,你可以使用指定的参数值创建一个虚拟机——无论该虚拟机是否已经存在(在你强制模块重新导入之后)。

虚拟硬盘

你现在已经将虚拟机附加到交换机,但没有存储的虚拟机是没有用的。为了解决这个问题,你需要创建一个本地虚拟硬盘(VHD)并将其连接到虚拟机。

注意

在第十六章中,你将使用一个社区脚本将 ISO 文件转换为 VHD。因此,你无需创建 VHD。但如果你不打算自动化操作系统部署,或者你需要将 VHD 创建作为其他脚本的一部分自动化,我仍然建议你完成这一部分。

手动创建 VHD

要创建 VHD 文件,你只需要一个命令:New-Vhd。在本节中,你将创建一个可以增长到 50GB 大小的 VHD;为了节省空间,你会将 VHD 设置为动态调整大小。

你首先需要在 Hyper-V 主机的C:\PowerLab\VHDs路径下创建一个文件夹来存放 VHD。确保为你的 VHD 命名时使用与你打算附加的虚拟机相同的名称,以保持简洁。

使用New-Vhd命令创建 VHD:

PS> New-Vhd ❶-Path 'C:\PowerLab\VHDs\MYVM.vhdx' ❷-SizeBytes 50GB ❸–Dynamic

ComputerName            : HYPERVSRV
Path                    : C:\PowerLab\VHDs\MYVM.vhdx
VhdFormat               : VHDX
VhdType                 : Dynamic
FileSize                : 4194304
Size                    : 53687091200
MinimumSize             :
LogicalSectorSize       : 512
PhysicalSectorSize      : 4096
BlockSize               : 33554432
ParentPath              :
DiskIdentifier          : 3FB5153D-055D-463D-89F3-BB733B9E69BC
FragmentationPercentage : 0
Alignment               : 1
Attached                : False
DiskNumber              :
Number                  :

你需要传递给New-Vhd路径❶和 VHD 大小❷,最后,指定你希望它动态调整大小❸。

使用Test-Path命令确认你是否成功在 Hyper-V 主机上创建了 VHD。如果Test-Path返回True,说明成功:

PS> Test-Path -Path 'C:\PowerLab\VHDs\MYVM.vhdx'
True

现在你需要将 VHD 附加到之前创建的虚拟机。为此,你需要使用Add-VMHardDiskDrive命令。但因为你不会将 VHD 附加到 LABDC——操作系统部署自动化将在第十六章中完成这项工作——所以你需要创建一个名为 MYVM 的虚拟机来附加 VHD:

PS> New-PowerLabVm -Name 'MYVM'
PS> ❶Get-VM -Name MYVM | Add-VMHardDiskDrive -Path 'C:\PowerLab\VHDs\MYVM.vhdx'
PS> ❷Get-VM -Name MYVM | Get-VMHardDiskDrive

VMName ControllerType ControllerNumber ControllerLocation DiskNumber Path
------ -------------- ---------------- ------------------ ---------- ----
MYVM   SCSI           0                0                             C:\PowerLab\VHDs\MYVM.vhdx

Add-VMHardDiskDrive命令接受Get-VM命令为其管道输入返回的对象类型,因此你可以直接从Get-VM将虚拟机传递给Add-VMHardDiskDrive——并指定 Hyper-V 主机上 VHD 的路径❶。

紧接着,使用Get-VMHardDiskDrive命令确认 VHDX 是否已成功添加❷。

自动化 VHD 创建

你可以向模块中添加另一个函数来自动化创建 VHD 并将其附加到虚拟机的过程。在创建脚本或函数时,考虑各种配置非常重要。

列表 15-4 定义了New-PowerLabVhd函数,该函数创建 VHD 并将虚拟机附加到它上面。

function New-PowerLabVhd {
    param
    (
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter()]
        [string]$AttachToVm,

        [Parameter()]
        [ValidateRange(512MB, 1TB)]
        [int64]$Size = 50GB,

 [Parameter()]
        [ValidateSet('Dynamic', 'Fixed')]
        [string]$Sizing = 'Dynamic',

        [Parameter()]
        [string]$Path = 'C:\PowerLab\VHDs'
    )

    $vhdxFileName = "$Name.vhdx"
    $vhdxFilePath = Join-Path -Path $Path -ChildPath "$Name.vhdx"

    ### Ensure we don't try to create a VHD when there's already one there
    if (-not (Test-Path -Path $vhdxFilePath -PathType Leaf)) { ❶
        $params = @{
            SizeBytes = $Size
            Path      = $vhdxFilePath
        }
        if ($Sizing -eq 'Dynamic') { ❷
            $params.Dynamic = $true
        } elseif ($Sizing -eq 'Fixed') {
            $params.Fixed = $true
        }

        New-VHD @params
        Write-Verbose -Message "Created new VHD at path [$($vhdxFilePath)]"
    }

    if ($PSBoundParameters.ContainsKey('AttachToVm')) {
        if (-not ($vm = Get-VM -Name $AttachToVm -ErrorAction SilentlyContinue)) { ❸
            Write-Warning -Message "The VM [$($AttachToVm)] does not exist. Unable to attach VHD."
        } elseif (-not ($vm | Get-VMHardDiskDrive | Where-Object { $_.Path -eq $vhdxFilePath })) { ❹
            $vm | Add-VMHardDiskDrive -Path $vhdxFilePath
            Write-Verbose -Message "Attached VHDX [$($vhdxFilePath)] to VM [$($AttachToVM)]."
        } else { ❺
            Write-Verbose -Message "VHDX [$($vhdxFilePath)] already attached to VM [$($AttachToVM)]."
        }
    }
}

列表 15-4:New-PowerLabVhd函数在PowerLab模块中的实现

该函数支持动态和固定大小❷,并且考虑到四种不同的状态:

  • VHD 已经存在❶。

  • 要附加 VHD 的虚拟机不存在❸。

  • 要附加 VHD 的虚拟机已经存在,但 VHD 尚未连接❹。

  • 要附加 VHD 的虚拟机已经存在,并且 VHD 已经附加❺。

函数设计是一个完全不同的领域。要能够创建一个在多种场景下都能正常运行的脚本或函数,需要多年的编码和实践。这是一门艺术,至今尚未完全完善,但如果你能预想到问题可能出现的多种方式,并在一开始就考虑到这些情况,你的函数就会更好。然而,不要过度投入,花费几个小时在一个函数或脚本上,确保每个细节都被覆盖!这只是代码,你可以在以后进行修改。

执行了 New-PowerLabVhd 函数

你可以在不同的状态下执行这段代码,并考虑每种状态。让我们测试多种状态,确保这个自动化脚本在每种情况下都能正常工作:

PS> New-PowerLabVhd -Name MYVM -Verbose -AttachToVm MYVM

VERBOSE: VHDX [C:\PowerLab\VHDs\MYVM.vhdx] already attached to VM [MYVM].

PS> Get-VM -Name MYVM | Get-VMHardDiskDrive | Remove-VMHardDiskDrive
PS> New-PowerLabVhd -Name MYVM -Verbose -AttachToVm MYVM

VERBOSE: Attached VHDX [C:\PowerLab\VHDs\MYVM.vhdx] to VM [MYVM].
PS> New-PowerLabVhd -Name MYVM -Verbose -AttachToVm NOEXIST

WARNING: The VM [NOEXIST] does not exist. Unable to attach VHD.

在这里,你并不是以正式的方式进行测试。相反,你通过强制让你的新函数运行你定义的每条代码路径,来测试它的表现。

使用 Pester 测试新函数

现在你应该能够自动化创建 Hyper-V 虚拟机了,但你应该始终为你创建的每个功能编写 Pester 测试,以确保一切按预期工作,并且随着时间的推移监控你的自动化。在本书的其余部分,你将为所有工作编写 Pester 测试。你可以在本书的资源中找到这些 Pester 测试,网址是https://github.com/adbertram/PowerShellForSysadmins/

在这一章中,你完成了四个任务:

  • 创建了一个虚拟交换机

  • 创建了一个虚拟机

  • 创建了一个 VHDX

  • 将 VHDX 附加到虚拟机

我把这一章的 Pester 测试分成了几个部分,每部分对应四个成果。像这样将测试分阶段有助于保持测试的条理性。

让我们运行这个测试,验证你在这一章编写的代码。要运行测试脚本,确保你已经从本书的资源中下载了Automating-Hyper-V.Tests.ps1脚本。在以下代码中,测试脚本位于*C:*的根目录,但你的路径可能不同,具体取决于你下载资源文件的位置。

PS> Invoke-Pester 'C:\Automating-Hyper-V.Tests.ps1'
Describing Automating Hyper-V Chapter Demo Work
   Context Virtual Switch
    [+] created a virtual switch called PowerLab 195ms
   Context Virtual Machine
    [+] created a virtual machine called MYVM 62ms
   Context Virtual Hard Disk
    [+] created a VHDX called MYVM at C:\PowerLab\VHDs 231ms
    [+] attached the MYVM VHDX to the MYVM VM 194ms
Tests completed in 683ms
Passed: 4 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0

所有四个测试都通过了,所以你可以继续进行下一章了。

总结

你已经为第一个真正的 PowerShell 自动化项目奠定了基础!希望你已经看到,通过 PowerShell 自动化可以节省多少时间!通过使用微软提供的免费 PowerShell 模块,你能够快速运行几个命令,轻松创建虚拟交换机、虚拟机和磁盘驱动器。微软给你了命令,但最终还是你自己搭建了周围的逻辑,使一切无缝衔接。

现在你可能已经意识到,可以即时编写有效的脚本,但通过提前思考并添加条件逻辑,你的脚本可以应对更多情况。

在下一章中,你将使用刚刚创建的虚拟机,自动化部署操作系统,几乎只需要一个 ISO 文件。

第十六章:安装操作系统

Images

在前一章中,你已设置好 PowerLab 模块,准备好开始。现在,你将迈出自动化旅程的下一步:学习自动化操作系统的安装。既然你已经创建了一个带有 VHD 的虚拟机,接下来需要安装 Windows。为此,你将使用 Windows Server ISO 文件、Convert-WindowsImage.ps1 PowerShell 脚本,以及大量脚本来创建一个完全自动化的 Windows 部署!

前提条件

我假设你已经跟随前一章的内容,并满足了所有的前提条件。在这里,你将需要一些额外的工具以便继续操作。首先,由于你将要部署操作系统,你需要一个 Windows Server 2016 ISO 文件。你可以通过登录免费的 Microsoft 账户,在http://bit.ly/2r5TPRP 下载一个免费试用版。

从前一章开始,我期望你在 Hyper-V 服务器上创建了一个C:\PowerLab文件夹。现在,你应该在其中创建一个 ISOs 子文件夹,C:\PowerLab\ISOs,并将你的 Windows Server 2016 ISO 文件放入其中。撰写本文时,ISO 文件名为en_windows_server_2016_x64_dvd_9718492.iso。你将在脚本中使用此文件路径,因此如果你的路径不同,请确保相应地更新脚本代码。

你还需要在 PowerLab 模块文件夹中有Convert-WindowsImage.ps1 PowerShell 脚本。如果你下载了本书的资源,这个脚本将与本章的资源一起提供。

还有一些事情:我期望你已经在 Hyper-V 服务器上创建了前一章中的 LABDC 虚拟机。你将使用它作为关联新创建的虚拟磁盘的地方。

最后,你需要一个无人值守的 XML 答案文件(也可以通过本章的可下载资源获取),名为LABDC.xml,位于 PowerLab 模块文件夹中。

和往常一样,运行本章附带的Prerequisites.Tests**.ps1 Pester 测试脚本,以确保你事先满足所有的前提条件。

操作系统部署

在自动化操作系统部署时,你将使用三个基本组件:

  • 一个包含操作系统位的 ISO 文件

  • 一个提供所有通常在安装时手动输入的答案文件

  • 微软的 PowerShell 脚本,用于将 ISO 文件转换为 VHDX

你的任务是找出一种方法,将所有这些组件组合在一起。大部分繁重的工作是由答案文件和 ISO 转换脚本完成的。你需要做的是创建一个小脚本,确保转换脚本使用适当的参数调用,并将新创建的 VHD 附加到相应的虚拟机。

你可以通过在下载的资源中找到名为Install-LABDCOperatingSystem.ps1的脚本来跟着操作。

创建 VHDX

LABDC 虚拟机将拥有一个 40GB 动态 VHDX 磁盘,分区为 GUID 分区表(GPT),运行 Windows Server 2016 Standard Core。转换脚本需要这些信息。它还需要知道源 ISO 文件的路径以及无人值守答案文件的路径。

首先,定义 ISO 文件和预填充答案文件的路径:

$isoFilePath = 'C:\PowerLab\ISOs\en_windows_server_2016_x64_dvd_9718492.iso'
$answerFilePath = 'C:\PowerShellForSysAdmins\PartII\Automating Operating System Installs\LABDC.xml'

接下来,你将构建转换脚本的所有参数。使用 PowerShell 的 splatting 技术,你可以创建一个单一的哈希表并将所有这些参数作为一个整体定义。这种定义和传递参数的方式比在一行中键入每个参数要更清晰:

$convertParams = @{
    SourcePath        = $isoFilePath
    SizeBytes         = 40GB
    Edition           = 'ServerStandardCore'
    VHDFormat         = 'VHDX'
    VHDPath           = 'C:\PowerLab\VHDs\LABDC.vhdx'
    VHDType           = 'Dynamic'
    VHDPartitionStyle = 'GPT'
    UnattendPath      = $answerFilePath
}

一旦为转换脚本定义了所有参数,你将对 Convert-WindowsImage.ps1 脚本进行点源(dot source)。你不想直接调用这个转换脚本,因为它包含一个名为 Convert-WindowsImage 的函数。如果你只是执行 Convert-WindowsImage.ps1 脚本,什么也不会发生,因为它只会加载脚本中的函数。

点源是一种将函数加载到内存中以供后续使用的方法;它会加载脚本中定义的所有函数到当前会话中,但不会实际执行它们。以下是如何点源 Convert-WindowsImage.ps1 脚本:

. "$PSScriptRoot\Convert-WindowsImage.ps1"

看看这段代码。这里有一个新变量:$PSScriptRoot。这是一个自动变量,表示脚本所在文件夹的路径。在这个例子中,由于Convert-WindowsImage.ps1脚本与 PowerLab 模块位于同一文件夹,所以你在 PowerLab 模块中引用了该脚本。

一旦转换脚本被点源到会话中,你就可以调用其中的函数,包括 Convert-WindowsImage。这个函数会为你完成所有繁重的工作:它会打开 ISO 文件,适当格式化新的虚拟磁盘,设置启动卷,注入你提供的答案文件,最终生成一个可以启动的新 Windows 系统的 VHDX 文件!

Convert-WindowsImage @convertParams

Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 10
Copyright (C) Microsoft Corporation.  All rights reserved.
Version 10.0.9000.0.amd64fre.fbl_core1_hyp_dev(mikekol).141224-3000 Beta

INFO   : Opening ISO en_windows_server_2016_x64_dvd_9718492.iso...
INFO   : Looking for E:\sources\install.wim...
INFO   : Image 1 selected (ServerStandardCore)...
INFO   : Creating sparse disk...
INFO   : Attaching VHDX...
INFO   : Disk initialized with GPT...
INFO   : Disk partitioned
INFO   : System Partition created
INFO   : Boot Partition created
INFO   : System Volume formatted (with DiskPart)...
INFO   : Boot Volume formatted (with Format-Volume)...
INFO   : Access path (F:\) has been assigned to the System Volume...
INFO   : Access path (G:\) has been assigned to the Boot Volume...
INFO   : Applying image to VHDX. This could take a while...
INFO   : Applying unattend file (LABDC.xml)...
INFO   : Signing disk...
INFO   : Image applied. Making image bootable...
INFO   : Drive is bootable. Cleaning up...
INFO   : Closing VHDX...

INFO   : Closing Windows image...
INFO   : Closing ISO...

INFO   : Done.

使用社区脚本,如 Convert-WindowsImage.ps1,是加速开发的好方法。这个脚本节省了大量时间,而且由于它是由 Microsoft 创建的,你可以信任它。如果你对这个脚本做了什么感到好奇,随时可以打开它。它做了很多事情,我个人很高兴我们有这样的资源来自动化操作系统安装。

附加虚拟机

当转换脚本完成时,你应该在 C:\PowerLab\VHDs 目录下找到一个准备启动的 LABDC.vhdx 文件。但你还没有完成。按现有状态,这个虚拟磁盘并没有附加到虚拟机。你必须将这个虚拟磁盘附加到一个现有的虚拟机(你将使用之前创建的 LABDC 虚拟机)。

就像你在前一章节中做的那样,使用 Add-VmHardDiskDrive 函数将虚拟磁盘附加到你的虚拟机:

$vm = Get-Vm -Name 'LABDC'
Add-VMHardDiskDrive -VMName 'LABDC' -Path 'C:\PowerLab\VHDs\LABDC.vhdx'

你需要从这个磁盘启动,所以让我们确保它在正确的启动顺序中。你可以使用Get-VMFirmware命令并查看BootOrder属性来发现现有的启动顺序:

$bootOrder = (Get-VMFirmware -VMName 'LABDC').Bootorder

注意,启动顺序中的第一个启动设备是网络启动。这不是你想要的。你希望虚拟机从你刚创建的磁盘启动。

$bootOrder.BootType

BootType
------
Network

要将你刚创建的 VHDX 设置为第一个启动设备,使用Set-VMFirmware命令和FirstBootDevice参数:

$vm | Set-VMFirmware -FirstBootDevice $vm.HardDrives[0]

到此为止,你应该已经拥有一个名为 LABDC 的虚拟机,并附加了一个将启动到 Windows 的虚拟磁盘。使用Start-VM -Name LABDC启动虚拟机,并确保它成功启动到 Windows。如果是这样,那么你完成了!

自动化操作系统部署

到目前为止,你已经成功创建了一个名为 LABDC 的虚拟机,它可以启动 Windows。现在需要意识到,你正在使用的脚本是专门为你的单个虚拟机量身定制的。在实际工作中,你很少能享有这种奢侈。一个好的脚本是可重用和可移植的,这意味着它不需要针对每个特定的输入进行更改,而是围绕一组不断变化的参数值进行工作。

让我们来看一下 PowerLab 模块中的Install-PowerLabOperatingSystem函数,它可以在本章的可下载资源中找到。这个函数很好地展示了如何将你正在使用的Install-LABDCOperatingSystem.ps1脚本转换为一个可以跨多个虚拟磁盘部署操作系统的脚本,只需简单地更改参数值。

在这一节中,我不会覆盖整个脚本,因为我们在上一节中已经讲解了大部分功能,但我确实想指出一些不同之处。首先,注意你使用了更多的变量。变量让你的脚本更具灵活性。它们为值提供了占位符,而不是将内容直接硬编码到代码中。

另外,还要注意脚本中的条件逻辑。查看 Listing 16-1 中的代码。这是一个switch语句,根据操作系统名称查找 ISO 文件路径。在之前的脚本中不需要这个,因为所有内容都是硬编码到脚本中的。

因为Install-PowerLabOperatingSystem函数有一个OperatingSystem参数,所以你可以灵活地安装不同的操作系统。你只需要找到一种方法来处理所有这些操作系统。一个很好的方法是使用switch语句,这样你可以轻松地添加更多条件。

switch ($OperatingSystem) {
    'Server 2016' {
        $isoFilePath = "$IsoBaseFolderPath\en_windows_server_2016_x64_dvd_9718492.iso"
    }
    default {
 throw "Unrecognized input: [$_]"
    }
}

Listing 16-1: 使用 PowerShell switch 逻辑

你可以看到,你已经将硬编码的值转移到了参数中。我不能再强调这一点:参数是构建可重用脚本的关键。尽量避免硬编码,并时刻关注那些你需要在运行时更改的值(然后使用参数来处理它们!)。但是,你可能会想,如果你只想偶尔更改某个值怎么办?接下来,你可以看到多个参数都有默认值。这允许你静态地设置“典型”值,然后根据需要进行覆盖。

param
(
    [Parameter(Mandatory)]
    [string]$VmName,

    [Parameter()]
    [string]$OperatingSystem = 'Server 2016',

    [Parameter()]
    [ValidateSet('ServerStandardCore')]
    [string]$OperatingSystemEdition = 'ServerStandardCore',

    [Parameter()]
    [string]$DiskSize = 40GB,

    [Parameter()]
    [string]$VhdFormat = 'VHDX',

    [Parameter()]
    [string]$VhdType = 'Dynamic',

    [Parameter()]
    [string]$VhdPartitionStyle = 'GPT',

    [Parameter()]
    [string]$VhdBaseFolderPath = 'C:\PowerLab\VHDs',

    [Parameter()]
    [string]$IsoBaseFolderPath = 'C:\PowerLab\ISOs',

    [Parameter()]
    [string]$VhdPath
)

使用 Install-PowerLabOperatingSystem 函数,你可以将所有这些内容变成一行代码,支持数十种配置。现在,你有了一块完整的、连贯的代码单元,可以用多种方式调用它,而不需要更改脚本中的任何一行!

将加密的凭据存储到磁盘

你很快就会完成项目的这一阶段,但在继续之前,你需要稍微绕个弯。这是因为你即将使用 PowerShell 执行一些需要凭据的操作。在脚本编写中,常常会把敏感信息(例如,用户名/密码组合)以明文形式存储在脚本中。类似地,可能会认为如果在测试环境中进行操作也无妨——但这为未来的工作埋下了危险的伏笔。即使在测试过程中,也要时刻关注安全措施,这样才能在从测试环境转向生产环境时养成良好的安全习惯。

避免在脚本中存储明文密码的一种简单方法是将其加密到文件中。当需要时,脚本可以解密并使用这些密码。幸运的是,PowerShell 提供了一种原生的方式来实现这一点:Windows 数据保护 API。该 API 在 Get-Credential 命令的底层被使用,这个命令会返回一个 PSCredential 对象。

Get-Credential 会创建一个被称为 安全字符串 的加密密码形式。一旦转换为安全字符串格式,整个凭据对象就可以通过 Export-CliXml 命令保存到磁盘;反之,使用 Import-CliXml 命令可以读取 PSCredential 对象。这些命令提供了一个便捷的密码管理系统。

在 PowerShell 中处理凭据时,你需要存储 PSCredential 对象,这些对象是大多数 Credential 参数接受的对象类型。在前面的章节中,你要么是交互式地输入用户名和密码,要么是以明文形式存储它们。但现在你已经入门了,让我们真正开始吧,为你的凭据添加保护。

PSCredential 对象以加密格式保存到磁盘需要使用 Export-CliXml 命令。使用 Get-Credential 命令,你可以创建一个用户名和密码的提示,并将结果传递给 Export-CliXml,后者接受保存 XML 文件的路径,如 列表 16-2 所示。

Get-Credential | Export-CliXml  -Path C:\DomainCredential.xml

列表 16-2:将凭据导出到文件

如果你打开 XML 文件,它应该像这样:

<TN RefId="0">
  <T>System.Management.Automation.PSCredential</T>
  <T>System.Object</T>
  </TN>
  <ToString>System.Management.Automation.PSCredential</ToString>
  <Props>
  <S N="UserName">userhere</S>
  <SS N="Password">ENCRYPTEDTEXTHERE</SS>
  </Props>
  </Obj>
</Objs>

现在凭证已经保存到磁盘上,让我们看看如何在 PowerShell 中获取它。使用 Import-CliXml 命令来解析 XML 文件并创建 PSCredential 对象:

$cred = Import-Clixml -Path C:\DomainCredential.xml
$cred | Get-Member

   TypeName: System.Management.Automation.PSCredential

Name                 MemberType Definition
----                 ---------- ----------
Equals               Method     bool Equals(System.Object obj)
GetHashCode          Method     int GetHashCode()
GetNetworkCredential Method     System.Net.NetworkCredential
                                GetNetworkCredential()
GetObjectData        Method     void GetObjectData(System.Runtime...
GetType              Method     type GetType()
ToString             Method     string ToString()
Password             Property   securestring Password {get;}
UserName             Property   string UserName {get;}

你将代码设置为只需将 $cred 传递给命令中的任何 Credential 参数。现在,代码将像你交互式输入一样工作。这种方法简洁明了,但通常你不会在生产环境中使用它,因为加密文本的用户必须也是解密者(这不是加密的本意!)。这种单一用户的要求并不适合大规模应用。但是,话虽如此,在测试环境中,它表现得非常好!

PowerShell 直接连接

现在,回到我们的项目。通常,当你在 PowerShell 中对远程计算机执行命令时,你需要使用 PowerShell 远程处理。这显然依赖于本地主机和远程主机之间的网络连接。如果你可以简化这个设置,完全不需要担心网络连接,岂不是很好?嗯,你可以!

因为你在 Windows Server 2016 Hyper-V 主机上运行所有自动化操作,你有一个非常有用的功能可以使用:PowerShell 直接连接。PowerShell 直接连接是 PowerShell 的一个较新功能,允许你在没有网络连接的情况下在 Hyper-V 服务器上托管的任何虚拟机上运行命令。你不需要提前为虚拟机设置网络适配器(尽管你已经通过无人值守的 XML 文件做了这件事)。

为了方便起见,你将大量使用 PowerShell 直接连接,而不是使用完整的网络堆栈。如果你不这样做,因为你处于工作组环境中,你必须在工作组环境中配置 PowerShell 远程处理——这不是件容易的事(请参见 bit.ly/2D3deUX)。在 PowerShell 中,选择战斗总是一个好主意,而在这里,我选择了最简单的方式!

PowerShell 直接连接与 PowerShell 远程处理几乎相同。它是一种在远程计算机上运行命令的方法。通常,这需要网络连接,但使用 PowerShell 直接连接时,不再需要网络连接。要通过 PowerShell 远程处理启动远程计算机上的命令,你通常会使用带有 ComputerNameScriptBlock 参数的 Invoke-Command

Invoke-Command -ComputerName LABDC -ScriptBlock { hostname }

然而,在使用 PowerShell 直接连接时,ComputerName 参数会变成 VMName,并且添加了一个 Credential 参数。通过 PowerShell 直接连接,完全可以像之前的代码一样运行相同的命令,但仅限于 Hyper-V 主机本身。为了简化操作,让我们先将 PSCredential 对象保存在磁盘上,这样以后就不需要反复提示输入凭证了。

对于这个例子,使用用户名powerlabuser和密码P@$$w0rd12

Get-Credential | Export-CliXml -Path C:\PowerLab\VMCredential.xml

现在你已经将凭证保存到磁盘,你将解密它并传递给 Invoke-Command。让我们读取保存在 VMCredential.xml 中的凭证,然后使用该凭证在 LABDC 虚拟机上执行代码:

$cred = Import-CliXml -Path C:\PowerLab\VMCredential.xml
Invoke-Command -VMName LABDC -ScriptBlock { hostname } -Credential $cred

为了让 PowerShell Direct 正常工作,背后有许多更复杂的操作,但我在这里不会深入探讨这些细节。如果你想全面了解 PowerShell Direct 是如何工作的,我推荐你查看 Microsoft 博客中宣布该功能的文章(https://docs.microsoft.com/en-us/virtualization**/hyper-v-on-windows/user-guide/powershell-direct)。

Pester 测试

现在是本章最重要的部分:让我们通过 Pester 测试把一切整合起来!你将遵循与上一章相同的模式,但在这里我想指出测试中的一个关键部分。在本章的 Pester 测试中,你将使用 BeforeAllAfterAll 块(清单 16-3)。

正如其名称所示,BeforeAll 块包含在所有测试之前执行的代码,而 AfterAll 块则包含在所有测试之后执行的代码。你使用这些块是因为你需要通过 PowerShell Direct 多次连接到 LABDC 服务器。PowerShell 远程处理和 PowerShell Direct 都支持会话的概念,你在 第一部分(第八章)中学到过。与其让 Invoke-Command 创建和销毁多个会话,不如提前定义一个会话并重复使用它。

BeforeAll {
    $cred = Import-CliXml -Path C:\PowerLab\VMCredential.xml
    $session = New-PSSession -VMName 'LABDC' -Credential $cred
}

AfterAll {
    $session | Remove-PSSession
}

清单 16-3: Tests.ps1——BeforeAll AfterAll

你会注意到你是在 BeforeAll 块中解密从磁盘保存的凭证。一旦创建了凭证,你将其与虚拟机的名称一起传递给 New-PSSession 命令。这是与 第一部分(第八章)中介绍的相同的 New-PSSession,但在这里你可以看到,你不是使用 ComputerName 作为参数,而是使用 VMName

这将创建一个单一的远程会话,你可以在整个测试过程中重用它。所有测试完成后,Pester 会查看 AfterAll 块并移除该会话。这种方法比反复创建会话要高效得多,尤其是当你需要执行数十个或数百个远程执行代码的测试时。

本章资源中的其余脚本内容很简单,遵循了你一直在使用的模式。如你所见,所有 Pester 测试都通过了,这意味着你仍然在正确的轨道上!

PS> Invoke-Pester 'C:\PowerShellForSysadmins\Part II\Automating Operating
System Installs\Automating Operating System Installs.Tests.ps1'
Describing Automating Operating System Installs
   Context Virtual Disk
    [+] created a VHDX called LABDC in the expected location 305ms
    [+] attached the virtual disk to the expected VM 164ms
    [+] creates the expected VHDX format 79ms
    [+] creates the expected VHDX partition style 373ms
    [+] creates the expected VHDX type 114ms
    [+] creates the VHDDX of the expected size 104ms
   Context Operating System
    [+] sets the expected IP defined in the unattend XML file 1.07s
    [+] deploys the expected Windows version 65ms
Tests completed in 2.28s
Passed: 8 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0

总结

在本章中,你深入了解了我们的实际项目。你使用了在上一章中构建的现有虚拟机,并且通过手动和自动方式为其部署了操作系统。到目前为止,你已经拥有了一台完全功能的 Windows 虚拟机,准备进入你旅程的下一阶段。

在下一章中,你将为你的 LABDC 虚拟机设置 Active Directory(AD)。设置 AD 将创建一个新的 AD 林和域,在本节结束时,你将有更多的服务器加入该域。

第十七章:部署 ACTIVE DIRECTORY

Images

在本章中,你将运用你在第二部分中学到的内容,开始在虚拟机上部署服务。由于许多其他服务依赖于 Active Directory,你必须首先部署一个 Active Directory 林和域。AD 林和域将支持你在接下来的章节中进行身份验证和授权的需求。

假设你已经阅读并在前一章中配置了 LABDC 虚拟机,你将使用它来完全自动化部署 Active Directory 林,并用一些测试用户和组填充它。

前提条件

你将使用你在第十六章中创建的内容,因此我假设你已经设置了一个 LABDC 虚拟机,使用无人值守的 XML 构建并启动,运行 Windows Server 2016。如果是这样,你就可以开始了!如果不是,你仍然可以从本章中获取有关如何自动化 Active Directory 的示例,但请注意:你将无法完全跟上。

和往常一样,运行相关的前提条件 Pester 测试,以确保你满足本章的所有前提条件。

创建一个 Active Directory 林

好消息是,考虑到所有因素,使用 PowerShell 创建 AD 林其实非常简单。归根结底,你实际上只需要运行两个命令:Install-WindowsFeatureInstall-ADDSForest。通过这两个命令,你可以构建一个林,创建一个域,并将 Windows 服务器配置为域控制器。

由于你将在实验环境中使用这个林,你还将创建一些组织单位、用户和组。处于实验环境意味着你没有任何生产对象可以使用。无需费劲地尝试将生产 AD 对象与实验环境同步,你可以创建许多模拟生产环境的对象,以便你有一些对象可以操作。

构建林

创建一个新的 AD 林时,首先需要提升一个 域控制器,这是 Active Directory 中的最低公分母。为了拥有一个正常运行的 AD 环境,你必须至少有一个域控制器。

由于这是一个实验环境,你将使用单个域控制器。在实际环境中,你希望至少有两个域控制器以确保冗余。然而,由于你的实验环境中没有数据,并且能够迅速从头开始重新创建它,因此这里只使用一个。在进行任何操作之前,你需要在 LABDC 服务器上安装 AD-Domain-Services Windows 功能。安装 Windows 功能的命令是 Install-WindowsFeature

PS> $cred = Import-CliXml -Path C:\PowerLab\VMCredential.xml 
PS> Invoke-Command -VMName 'LABDC' -Credential $cred -ScriptBlock 
{ Install-windowsfeature -Name AD-Domain-Services }
PSComputerName : LABDC RunspaceId : 33d41d5e-50f3-475e-a624-4cc407858715
Success : True RestartNeeded : No FeatureResult : {Active Directory Domain
Services, Remote Server Administration Tools, Active Directory module for
Windows PowerShell, AD DS and AD LDS Tools...} ExitCode : Success ```

After providing a credential to connect to the server, you use Invoke-Command to remotely run the Install-WindowsFeature commands on the remote server.

Once the feature is installed, you can create the forest by using the Install-ADDSForest command. This command is part of the ActiveDirectory PowerShell module, which was installed on LABDC as part of the feature installation.

The Install-ADDSForest command is the only command you need to create a forest. It takes a few parameters, which you’ll fill in using code but are usually filled in using a GUI. This forest will be called powerlab.local. Since the domain controller is Windows Server 2016, you’ll set the domain mode and forest mode both to WinThreshold. For a full breakdown of all the available DomainMode and ForestMode values, refer to the Install-ADDSForest Microsoft documentation page (bit.ly/2rrgUi6).

Saving Secure Strings to Disk

In Chapter 16, when you needed credentials, you saved PSCredential objects and reused them in your commands. This time around, you don’t need a PSCredential object. Instead, you need only a single encrypted string.

In this section, you’ll see that you need to pass a safe mode administrator password to a command. As with any piece of sensitive information, you want to use encryption. As you did in the preceding chapter, you’ll use Export-CliXml and Import-CliXml to save and retrieve PowerShell objects from the filesystem. Here, though, instead of calling Get-Credential, you’ll create a secure string by using ConvertTo-SecureString and then save that object to a file.

To save an encrypted password to a file, you pass the plaintext password to ConvertTo-SecureString and then export that secure string object to Export-CliXml, creating a file you can reference later:


PS> 'P@$$w0rd12' | ConvertTo-SecureString -Force -AsPlainText

| Export-Clixml -Path C:\PowerLab\SafeModeAdministratorPassword.xml

As you can see, after you have the safe mode administrator password saved to disk, you can read it with Import-CliXml and pass in all the other parameters that Install-ADDSForest needs to run. You do this with the following code:


PS> $safeModePw = Import-CliXml -Path C:\PowerLab\

SafeModeAdministratorPassword.xml

PS> $cred = Import-CliXml -Path C:\PowerLab\VMCredential.xml

PS> $forestParams = @{

>>> DomainName                    = 'powerlab.local' ❶

>>> DomainMode                    = 'WinThreshold' ❷

>>> ForestMode                    = 'WinThreshold'

>>> Confirm                       = $false ❸

>>> SafeModeAdministratorPassword = $safeModePw ❹

>>> WarningAction                 = 'Ignore ❺

>>>}

PS> Invoke-Command -VMName 'LABDC' -Credential $cred -ScriptBlock { $null =

Install-ADDSForest @using:forestParams }

Here, you’re creating a forest and domain called powerlab.local ❶ running at a Windows Server 2016 functional level (WinThreshold) ❷, bypassing all confirmations ❸, passing your safe mode administrator password ❹, and ignoring the irrelevant warning messages that typically come up ❺.

Automating Forest Creation

Now that you’ve done it manually, let’s build a function in your PowerLab module that will handle AD forest creation for you. Once you have a function, you’ll be able to use it across numerous environments.

In the PowerLab module included with this chapter’s resources, you’ll see a function called New-PowerLabActiveDirectoryForest, as shown in Listing 17-1.


function New-PowerLabActiveDirectoryForest {

    param(

        [Parameter(Mandatory)]

        [pscredential]$Credential,

        [Parameter(Mandatory)]

        [string]$SafeModePassword,

        [Parameter()]

        [string]$VMName = 'LABDC',

        [Parameter()]

        [string]$DomainName = 'powerlab.local',

        [Parameter()]

        [string]$DomainMode = 'WinThreshold',

        [Parameter()]

        [string]$ForestMode = 'WinThreshold'

    )

    Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock {

        Install-windowsfeature -Name AD-Domain-Services

        $forestParams = @{

            DomainName                    = $using:DomainName

            DomainMode                    = $using:DomainMode

            ForestMode                    = $using:ForestMode

            Confirm                       = $false

            SafeModeAdministratorPassword = (ConvertTo-SecureString

                                            -AsPlainText -String $using:

                                            SafeModePassword -Force)

            WarningAction                 = 'Ignore'

        }

        $null = Install-ADDSForest @forestParams

    }

}

Listing 17-1: The New-PowerLabActiveDirectoryForest function

As in the preceding chapter, you simply define several parameters you’ll use to pass to the ActiveDirectory module’s Install-ADDSForest command. Notice that you define two Mandatory parameters for the credentials and password. As its name suggests, these are parameters the user needs to pass in (because the other parameters have default values, the user does not necessarily need to pass them in). You’ll use this function by reading in your saved administrator password and credential, and then passing the two into the function:


PS> $safeModePw = Import-CliXml -Path C:\PowerLab\SafeModeAdministratorPassword.xml

PS> $cred = Import-CliXml -Path C:\PowerLab\VMCredential.xml

PS> New-PowerLabActiveDirectoryForest -Credential $cred -SafeModePassword $safeModePw

After running this code, you’ll have a fully working Active Domain forest! Well, you should, anyway—let’s figure out a way to confirm that the forest is up and running. A good test is to query all the default user accounts in the domain. To do so, however, you need to create another PSCredential object stored on disk; because LABDC is a domain controller now, you need a domain user account (not a local user account). You’ll create and save a credential with the username of powerlab.local\administrator and a password of P@$$w0rd12 to the C:\PowerLab\DomainCredential.xml file. Remember that you need to do this only once. Then, you can use the new domain credential to connect to LABDC:


PS> Get-Credential | Export-CliXml -Path C:\PowerLab\DomainCredential.xml

Once the domain credential is created, you’ll create another function in your PowerLab module called Test-PowerLabActiveDirectoryForest. Right now, this function just gathers all the users in a domain, but because you have this functionality wrapped in a function, you can customize this test to your liking:


function Test-PowerLabActiveDirectoryForest {

    param(

        [Parameter(Mandatory)]

        [pscredential]$Credential,

        [Parameter()]

        [string]$VMName = 'LABDC'

    )

    Invoke-Command -Credential $Credential -ScriptBlock {Get-AdUser -Filter * }

}

Try executing the Test-PowerLabActiveDirectoryForest function by using the domain credential and a VMName of LABDC. If you’re shown a few user accounts, congrats! You’re done! You’ve now successfully set up a domain controller and stored credentials for connecting to VMs in a workgroup (and any future domain-joined VMs).

Populating the Domain

In the preceding section, you set up a domain controller in your PowerLab. Now let’s create some test objects. Since this is a test lab, you want to create various objects (OUs, users, groups, and so on) so that you cover all your bases. You could run the required command to create each individual object, but because you have so many objects to create, that wouldn’t be practical. It’ll be a much better use of your time to define everything in one file, read in each object, and create them all in one go.

Handling Your Object Spreadsheet

Here, you’ll use an Excel spreadsheet as your input file to define everything you need as input. This Excel spreadsheet is available via the chapter’s downloadable resources. When you open it, you’ll see it has two worksheets: Users (Figure 17-1) and Groups (Figure 17-2).

Image

Figure 17-1: The Users spreadsheet

Image

Figure 17-2: The Groups spreadsheet

Each row of these worksheets corresponds to a user or group that needs to be created, containing information you’ll read into PowerShell. As you saw in Chapter 10, native PowerShell cannot handle Excel spreadsheets without significant work. With the help of a popular community module, however, you can make this much easier. Using the ImportExcel module, you can read Excel spreadsheets just as easily as you can natively read CSV files. To get ImportExcel, you can download it from the PowerShell Gallery by using Install-Module -Name ImportExcel. After a few security prompts, you should have the module downloaded and ready to use.

Now let’s use the Import-Excel command to parse in the rows from the worksheet:


PS> Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\

ActiveDirectoryObjects.xlsx' -WorksheetName Users | Format-Table -AutoSize

OUName         UserName   FirstName LastName  MemberOf

------         --------   --------- --------  --------

PowerLab 用户 jjones     Joe       Jones     会计

PowerLab 用户 abertram   Adam      Bertram   会计

PowerLab 用户 jhicks     Jeff      Hicks     会计

PowerLab 用户 dtrump     Donald    Trump     人力资源

PowerLab 用户 alincoln   Abraham   Lincoln   人力资源

PowerLab 用户 bobama     Barack    Obama     人力资源

PowerLab 用户 tjefferson Thomas    Jefferson IT

PowerLab 用户 bclinton   Bill      Clinton   IT

PowerLab 用户 gbush      George    Bush      IT

PowerLab 用户 rreagan    Ronald    Reagan    IT

PS> Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\

ActiveDirectoryObjects.xlsx' -WorksheetName Groups | Format-Table -AutoSize

OUName          GroupName       Type

------          ---------       ----

PowerLab 组 会计      DomainLocal

PowerLab 组 人力资源 DomainLocal

PowerLab 组 IT              DomainLocal

Using the Path and WorksheetName parameters, you can easily pull out the data you need. Notice that here, you’re using the Format-Table command. This is a useful command that forces PowerShell to display the output in a table format. The AutoSize parameter tells PowerShell to try to squeeze each row into one line in the console.

Creating a Plan

You now have a way to read the data from the Excel spreadsheet. The next step is figuring out what to do with it. You’ll build a function in your PowerLab module that reads each row and performs the action it requires. All code covered here is available via the New-PowerLabActiveDirectoryTestObject function in the associated PowerLab module.

This function is a little more complicated than our previous scripts, so let’s break it down in an informal way—this way, you have something to refer back to. This step may not sound important, but as you make bigger functions, you’ll find that planning them out at the start will save you a lot of work in the long run. In this function, you need to do the following:

  1. Read both worksheets in an Excel spreadsheet and retrieve all user and group rows.

  2. Read each row in both worksheets and first confirm whether the OU that the user or group is supposed to be a part of exists.

  3. If the OU does not exist, create the OU.

  4. If the user/group does not exist, create the user or group.

  5. For users only: add the user as a member of the specified group.

Now that you have this informal outline, let’s get down to coding.

Creating the AD Objects

For the first pass through, you want to keep it simple: let’s focus on handling a single object. No need to complicate things now by worrying about all of them. You installed the AD-Domain-Services Windows feature on LABDC earlier, so now you have the ActiveDirectory module installed. This module provides a large set of useful commands (as you saw in Chapter 11). Recall that many of the commands follow the same naming convention of Get/Set/New-AD.

Let’s open a blank .ps1 script and get to work. Start by writing out all the commands you need (Listing 17-2) based on the previous outline:


Get-ADOrganizationalUnit -Filter "Name -eq 'OUName'" ❶

New-ADOrganizationalUnit -Name OUName ❷

Get-ADGroup -Filter "Name -eq 'GroupName'" ❸

New-ADGroup -Name $group.GroupName -GroupScope GroupScope -Path "OU=$group.OUName,DC=powerlab,DC=local" ❹

Get-ADUser -Filter "Name -eq 'UserName'" ❺

New-ADUser -Name $user.UserName -Path "OU=$($user.OUName),DC=powerlab,DC=local" ❻

UserName -in (Get-ADGroupMember -Identity GroupName).Name ❼

Add-ADGroupMember -Identity GroupName -Members UserName ❽

Listing 17-2: Figuring out code to check for and create new users and groups

Recall from our plan that you first need to check whether an OU exists ❶, and then create one if it doesn’t ❷. You do the same thing with each group: check whether it exists ❸ and create one if it doesn’t ❹. And do the same thing for each user: check ❺ and create ❻. Lastly, for your users, check whether they are a member of the group specified in the spreadsheet ❼, and add them to it if they are not ❽.

All you’re missing here is the conditional structure, which you add in Listing 17-3.


如果 (-not (Get-ADOrganizationalUnit -Filter "Name -eq 'OUName'")) {

    New-ADOrganizationalUnit -Name OUName

}

如果 (-not (Get-ADGroup -Filter "Name -eq 'GroupName'")) {

    New-ADGroup -Name GroupName -GroupScope GroupScope -Path "OU=OUName,DC=powerlab,DC=local"

}

如果 (-not (Get-ADUser -Filter "Name -eq 'UserName'")) {

    New-ADUser -Name $user.UserName -Path "OU=OUName,DC=powerlab,DC=local"

}

如果 (UserName -notin (Get-AdGroupMember -Identity GroupName).Name) {

    Add-ADGroupMember -Identity GroupName -Members UserName

}

Listing 17-3: Creating users and groups only if they don’t already exist

Now that you have the code to do what you want for an individual user or group, you need to figure out how to do it for all of them. First, though, you need to read in the worksheets. You’ve already seen which commands to use; now you need to store all those rows in variables. This isn’t technically required, but it keeps your code more explicit and self-documenting. You’ll use foreach loops to read all users and groups, as shown in Listing 17-4.


$users = Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\

PowerLab\ActiveDirectoryObjects.xlsx' -WorksheetName Users

$groups = Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\

PowerLab\ActiveDirectoryObjects.xlsx' -WorksheetName Groups

遍历 ($group 在 $groups 中) {

}

遍历 ($user 在 $users 中) {

}

Listing 17-4: Building the code structure to iterate over each Excel worksheet row

Now that you have a structure to loop through every row, let’s use our individual code to handle the rows, as shown in Listing 17-5.


$users = Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\

ActiveDirectoryObjects.xlsx' -WorksheetName Users

$groups = Import-Excel -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\

ActiveDirectoryObjects.xlsx' -WorksheetName Groups

遍历 ($group 在 $groups 中) {

    如果 (-not (Get-ADOrganizationalUnit -Filter "Name -eq '$($group.OUName)'")) {

        New-ADOrganizationalUnit -Name $group.OUName

    }

    如果 (-not (Get-ADGroup -Filter "Name -eq '$($group.GroupName)'")) {

        New-ADGroup -Name $group.GroupName -GroupScope $group.Type

        -Path "OU=$($group.OUName),DC=powerlab,DC=local"

    }

}

遍历 ($user 在 $users 中) {

    如果 (-not (Get-ADOrganizationalUnit -Filter "Name -eq '$($user.OUName)'")) {

        New-ADOrganizationalUnit -Name $user.OUName

    }

    如果 (-not (Get-ADUser -Filter "Name -eq '$($user.UserName)'")) {

        New-ADUser -Name $user.UserName -Path "OU=$($user.OUName),DC=powerlab,DC=local"

    }

    如果 ($user.UserName -notin (Get-ADGroupMember -Identity $user.MemberOf).Name) {

        Add-ADGroupMember -Identity $user.MemberOf -Members $user.UserName

    }

}

Listing 17-5: Performing tasks on all users and groups

You’re almost done! The script is all ready to go, but now you need to run it on the LABDC server. Since you’re not running this code directly on the LABDC VM itself yet, you have to wrap all this up into a scriptblock and have Invoke-Command run it remotely on LABDC for you. Since you want to create and populate the forest in one go, you’ll take all your “scratch” code and move it into your New-PowerLabActiveDirectoryTestObject function. You can download a copy of this fully created function in the chapter’s resources.

Building and Running Pester Tests

You have all the code you need to create a new AD forest and populate it. Now you’ll build some Pester tests to confirm that everything is working as planned. You have quite a bit to test, so the Pester tests are going to be more complicated than before. Just as you did before creating the New-PowerLabActiveDirectoryTestObject.ps1 script, first create a Pester test script, and then start thinking of test cases. If you need a refresher about Pester, check out Chapter 9. I’ve also included all Pester tests for this chapter in the book’s resources.

What do you need to test? In this chapter, you did the following:

  • Created a new AD forest

  • Created a new AD domain

  • Created AD users

  • Created AD groups

  • Created AD organizational units

After determining that they exist, you need to make sure that your objects have the correct attributes (the attributes you passed in as parameters to the commands that created them). These are the attributes you’re looking for:

Table 17-1: AD Attributes

| Object | Attributes |
| AD forest | DomainName, DomainMode, ForestMode, safe mode administrator password |
| AD user | OU path, name, group member |
| AD group | OU path, name |
| AD organizational unit | Name |

With that, you have a good back-of-the-napkin plan for what you’re looking for with your Pester tests. If you take a look at the Creating an Active Directory Forest.Tests.ps1 script, you’ll see that I’ve chosen to break down each of these entities into contexts and test all the associated attributes inside as individual tests.

To give you an idea of how these tests are created, Listing 17-6 has a snippet of the test code.


上下文 '域' {

❶ $domain = Invoke-Command -Session $session -ScriptBlock { Get-AdDomain }

    $forest = Invoke-Command -Session $session -ScriptBlock { Get-AdForest }

❷ 它 "域模式应为 Windows2016Domain" {

        $domain.DomainMode | 应该是 'Windows2016Domain'

    }

    它 "林模式应为 WinThreshold" {

        $forest.ForestMode | 应该是 'Windows2016Forest'

    }

    它 "域名应为 powerlab.local" {

        $domain.Name | 应该是 'powerlab'

    }

}

Listing 17-6: Some of the Pester test code

For this context, you want to make sure that the AD domain and forest are created properly. So you first create the domain and forest ❶; then you verify that the domain and forest have the attributes you expect ❷.

Running the whole test should give you something like this:


描述 Active Directory 林

上下文 域

    [+] 域模式应为 Windows2016Domain 933ms

    [+] 林模式应为 WinThreshold 25ms

    [+] 域名应该是 powerlab.local 41ms

上下文 组织单位

    [+] OU[PowerLab Users]应该存在 85ms

    [+] OU[PowerLab Groups]应该存在 37ms

上下文 用户

    [+] 用户[jjones]应该存在 74ms

    [+] 用户[jjones]应该在[PowerLab Users] OU 中 35ms

    [+] 用户[jjones]应该在[Accounting]组中 121ms

    [+] 用户[abertram]应该存在 39ms

    [+] 用户[abertram]应该在[PowerLab Users] OU 中 30ms

    [+] 用户[abertram]应该在[Accounting]组中 80ms

    [+] 用户[jhicks]应该存在 39ms

    [+] 用户[jhicks]应该在[PowerLab Users] OU 中 32ms

    [+] 用户[jhicks]应该在[Accounting]组中 81ms

    [+] 用户[dtrump]应该存在 45ms

    [+] 用户[dtrump]应该在[PowerLab Users] OU 中 40ms

    [+] 用户[dtrump]应该在[Human Resources]组中 84ms

    [+] 用户[alincoln]应该存在 41ms

    [+] 用户[alincoln]应该在[PowerLab Users] OU 中 40ms

    [+] 用户[alincoln]应该在[Human Resources]组中 125ms

    [+] 用户[bobama]应该存在 44ms

    [+] 用户[bobama]应该在[PowerLab Users] OU 中 27ms

    [+] 用户[bobama]应该在[Human Resources]组中 92ms

[+] 用户[tjefferson]应该存在 58ms

    [+] 用户[tjefferson]应该在[PowerLab Users] OU 中 33ms

    [+] 用户[tjefferson]应该在[IT]组中 73ms

    [+] 用户[bclinton]应该存在 47ms

    [+] 用户[bclinton]应该在[PowerLab Users] OU 中 29ms

    [+] 用户[bclinton]应该在[IT]组中 84ms

    [+] 用户[gbush]应该存在 50ms

    [+] 用户[gbush]应该在[PowerLab Users] OU 中 33ms

    [+] 用户[gbush]应该在[IT]组中 78ms

    [+] 用户[rreagan]应该存在 56ms

    [+] 用户[rreagan]应该在[PowerLab Users] OU 中 30ms

    [+] 用户[rreagan]应该在[IT]组中 78ms

上下文 组

    [+] 组[Accounting]应该存在 71ms

    [+] 组[Accounting]应该在[PowerLab Groups] OU 中 42ms

    [+] 组[Human Resources]应该存在 48ms

    [+] 组[Human Resources]应该在[PowerLab Groups] OU 中 29ms

    [+] 组[IT]应该存在 51ms

    [+] 组[IT]应该在[PowerLab Groups] OU 中 31ms

总结

在本章中,你在创建 PowerLab 的过程中迈出了下一步,添加了一个 Active Directory 森林,并在其中填充了多个对象。你既进行了手动操作,也进行了自动操作,在这个过程中复习了一些你在前几章中学到的关于 Active Directory 的内容。最后,你深入了解了 Pester 测试,仔细研究了如何构建符合你需求的自定义测试。在下一章中,你将继续 PowerLab 项目,学习如何自动化安装和配置 SQL 服务器。

第十八章:创建和配置 SQL 服务器

图片

到目前为止,你已经创建了一个可以创建虚拟机、附加 VHD、安装 Windows 并创建(和填充)活动目录森林的模块。让我们再增加一项:部署 SQL 服务器。有了一个虚拟机、安装了操作系统并设置了域控制器,你已经完成了大部分的繁重工作!现在你只需利用现有的功能,通过少许调整,就能安装 SQL 服务器。

先决条件

在本章中,我假设你已经在第三部分中跟随操作并创建了至少一个名为 LABDC 的虚拟机,该虚拟机正在你的 Hyper-V 主机上运行。这个虚拟机将作为域控制器运行,由于你将通过 PowerShell Direct 再次连接到多个虚拟机,因此你需要将域凭据保存到 Hyper-V 主机(查看第十七章以了解我们是如何做到这一点的)。

你将使用一个名为ManuallyCreatingASqlServer.ps1的脚本(可以在本章资源中找到)来解释如何正确地自动化部署 SQL 服务器。这个脚本包含了本章中介绍的所有基本步骤,是你在完成本章过程中一个很好的参考资源。

和往常一样,请运行本章附带的先决条件测试脚本,以确保你满足所有预期的先决条件。

创建虚拟机

当你想到SQL Server时,你可能会想到数据库、作业和表等内容。但在你能够处理这些内容之前,必须完成大量的后台工作:首先,每个 SQL 数据库都必须存在于服务器上,每个服务器需要一个操作系统,每个操作系统需要一个物理或虚拟机来安装。幸运的是,你在过去的几章中已经设置了创建 SQL 服务器所需的确切环境。

一位优秀的自动化工程师会从分解所有必要的依赖项开始每个项目。他们围绕这些依赖项进行自动化,然后再基于它们进行扩展。这个过程会导致一个模块化、解耦的架构,具有随时相对轻松地进行更改的灵活性。

最终你需要的是一个函数,它使用标准配置启动任意数量的 SQL 服务器。但要实现这一点,你必须分层思考这个项目。第一层是虚拟机。我们先处理这个。

既然你已经在 PowerLab 模块中有一个构建虚拟机的函数,你就可以使用它。因为你构建的所有实验室环境都将是相同的,而且你已经将创建新虚拟机所需的许多参数定义为New-PowerLabVM函数的默认参数值,所以你唯一需要传递给这个函数的就是虚拟机的名称:

PS> New-PowerLabVm -Name 'SQLSRV'

安装操作系统

就这样,你有了一个准备好的虚拟机。那还真是简单。我们再做一次。使用你在第十六章中编写的命令在虚拟机上安装 Windows:

PS> Install-PowerLabOperatingSystem -VmName 'SQLSRV'
Get-Item : Cannot find path 'C:\Program Files\WindowsPowerShell\Modules\
powerlab\SQLSRV.xml' because it does not exist.
At C:\Program Files\WindowsPowerShell\Modules\powerlab\PowerLab.psm1:138 char:16
+     $answerFile = Get-Item -Path "$PSScriptRoot\$VMName.xml"
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\Program File...rlab\SQLSRV
                              .xml:String) [Get-Item], ItemNotFoundException

哎呀!你使用了 PowerLab 模块中现有的Install-PowerLabOperatingSystem函数来安装即将成为 SQL 服务器的操作系统,但它失败了,因为它引用了模块文件夹中的一个名为SQLSRV.xml的文件。当你构建这个函数时,你假设模块文件夹中会有一个.xml文件。在构建像这样的庞大自动化项目时,路径不一致和文件不存在等问题是常见的。你会有很多依赖项需要处理。解决这些错误的唯一方法就是尽可能多地执行代码,尽可能多地测试不同场景。

添加一个 Windows 无人值守应答文件

Install-PowerLabOperatingSystem函数假设 PowerLab 模块文件夹中总是会有一个名为.xml的文件。这意味着,在部署新服务器之前,你必须先确保将该文件放在正确的位置。幸运的是,现在你已经创建了 LABDC 无人值守应答文件,这应该很容易。你首先需要做的是复制现有的LABDC.xml文件,并将其命名为SQLSRV.xml

PS> Copy-Item -Path 'C:\Program Files\WindowsPowerShell\Modules\PowerLab\LABDC.xml' -Destination
'C:\Program Files\WindowsPowerShell\Modules\PowerLab\SQLSRV.xml'

一旦你复制了,接下来你需要做一些调整:主机名和 IP 地址。由于你没有部署 DHCP 服务器,所以你将使用静态 IP 地址并必须更改它们(否则你只需要更改服务器名称)。

打开 C:\Program Files\WindowsPowerShell\Modules\SQLSRV.xml,并查找定义主机名的部分。一旦找到它,修改ComputerName值。它应该类似于下面这样:

<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 
    xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <ComputerName>SQLSRV</ComputerName>
    <ProductKey>XXXXXXXXXXXXX</ProductKey>
</component>

接下来,查找UnicastIPAddress节点。它看起来像以下代码。请注意,我使用的是一个 10.0.0.0/24 的网络,并选择让我的 SQL 服务器的 IP 地址为 10.0.0.101:

<UnicastIpAddresses>
    <IpAddress wcm:action="add" wcm:keyValue="1">10.0.0.101</IpAddress>
</UnicastIpAddresses>

保存SQLSRV.xml文件,并再次尝试运行Install-PowerLabOperatingSystem命令。此时,你的命令应该能够成功运行,并将 Windows Server 2016 部署到你的 SQLSRV 虚拟机上。

将 SQL Server 添加到域

你刚安装了操作系统,现在需要启动虚拟机。使用Start-VM cmdlet 很容易做到:

PS> Start-VM -Name SQLSRV

现在你必须等待虚拟机上线——这可能需要一些时间。需要多久?这取决于很多变量。你可以做的一件事是使用while循环不断检查是否能够连接到虚拟机。

让我们看看如何操作。在 Listing 18-1 中,你获取了虚拟机的本地保存凭据。一旦你有了凭据,你可以创建一个while循环,持续执行Invoke-Command直到返回结果。

注意你正在为ErrorAction参数使用Ignore值。你必须这么做,因为如果没有它,当Invoke-Command无法连接到计算机时,它会返回一个非终止错误信息。为了避免控制台被预期中的错误信息填满(因为你知道可能无法连接,并且对此没问题),你正在忽略这些错误信息。

$vmCred = Import-CliXml -Path 'C:\PowerLab\VMCredential.xml'
while (-not (Invoke-Command -VmName SQLSRV -ScriptBlock { 1 } -Credential
$vmCred -ErrorAction Ignore)) {
    Start-Sleep -Seconds 10
    Write-Host 'Waiting for SQLSRV to come up...'
}

清单 18-1:检查服务器是否在线,并忽略错误信息

一旦虚拟机终于启动,就可以将其添加到你在上一章创建的域中。添加计算机到域的命令是 Add-Computer。由于你是在 Hyper-V 主机上运行所有命令,而不依赖于网络连接,因此需要将 Add-Computer 命令包裹在脚本块中,并通过 PowerShell Direct 执行它,直接在 SQLSRV 上运行。

请注意,在清单 18-2 中,你需要同时使用虚拟机的本地用户帐户和域帐户。为此,你首先通过 Invoke-Command 连接到 SQLSRV 服务器本身。连接后,你会将域凭证传递给域控制器以进行身份验证,这样就可以将计算机帐户添加到域中。

$domainCred = Import-CliXml -Path 'C:\PowerLab\DomainCredential.xml'
$addParams = @{
    DomainName = 'powerlab.local'
    Credential = $domainCred
    Restart    = $true
    Force      = $true
}
Invoke-Command -VMName SQLSRV -ScriptBlock { Add-Computer ❶@using:addParams } -Credential $vmCred

清单 18-2:获取凭证并将计算机添加到域

请注意,你正在使用 $using 关键字 ❶。该关键字允许你将本地变量 $addParams 传递到 SQLSRV 服务器的远程会话中。

由于你在 Add-Computer 中使用了 Restart 开关参数,虚拟机将在添加到域后立即重启。同样,由于你还有进一步的工作要做,你需要等待这一过程发生。然而,这一次,你需要等它先关闭 然后 再重启(见清单 18-3),因为脚本非常快速,如果你不等待它先关闭,脚本可能会继续运行,因为它检测到服务器已经启动,但实际上它并没有关闭!

❶ while (Invoke-Command -VmName SQLSRV -ScriptBlock { 1 } -Credential $vmCred 
   -ErrorAction Ignore) {
    ❷ Start-Sleep -Seconds 10
    ❸ Write-Host 'Waiting for SQLSRV to go down...'
}

❶ while (-not (Invoke-Command -VmName SQLSRV -ScriptBlock { 1 } -Credential 
   $domainCred -ErrorAction Ignore)) {
    ❷ Start-Sleep -Seconds 10
    ❸ Write-Host 'Waiting for SQLSRV to come up...'
}

清单 18-3:等待服务器重启

首先,你通过在 SQLSRV ❶ 上返回数字 1 来检查 SQLSRV 是否已关闭。如果返回输出,这意味着 PowerShell 远程访问可用,因此 SQLSRV 尚未关闭。如果有输出返回,接下来你需要暂停 10 秒 ❷,在屏幕上写一条消息 ❸,然后再试一次。

当测试 SQLSRV 何时重新启动时,你会采取相反的操作。一旦脚本释放了控制台,SQLSRV 应该已经启动并被添加到你的 Active Directory 域中。

安装 SQL Server

现在你已经创建了一个带有 Windows Server 2016 的虚拟机,你可以在其上安装 SQL Server 2016。这是新代码!直到现在,你一直在利用现有的代码;现在你又在开辟新天地。

通过 PowerShell 安装 SQL Server 包括几个步骤:

  1. 复制并调整 SQL Server 答案文件

  2. 复制 SQL Server ISO 文件到即将成为 SQL 服务器的虚拟机

  3. 挂载即将成为 SQL 服务器的 ISO 文件

  4. 运行 SQL Server 安装程序

  5. 卸载 ISO 文件

  6. 清理 SQL 服务器上的任何临时复制文件

复制文件到 SQL 服务器

根据我们的计划,第一步是将一些文件复制到即将成为 SQL 服务器的计算机上。你需要 SQL Server 安装程序需要的无人参与回答文件,还需要包含 SQL Server 安装内容的 ISO 文件。由于我们假设 Hyper-V 主机与虚拟机之间没有网络连接,因此你将再次使用 PowerShell Direct 来复制这些文件。要使用 PowerShell Direct 复制文件,你首先需要在远程虚拟机上创建一个会话。在下面的代码中,你使用 Credential 参数来验证 SQLSRV。如果你的服务器与当前操作的计算机在同一个 Active Directory 域中,那么就不需要 Credential 参数。

$session = New-PSSession -VMName 'SQLSRV' -Credential $domainCred

接下来,复制 PowerLab 模块中找到的模板 SQLServer.ini 文件:

$sqlServerAnswerFilePath = "C:\Program Files\WindowsPowerShell\Modules\
PowerLab\SqlServer.ini"
$tempFile = Copy-Item -Path $sqlServerAnswerFilePath -Destination "C:\Program
Files\WindowsPowerShell\Modules\PowerLab\temp.ini" -PassThru

完成之后,你将修改文件以匹配所需的配置。回想一下,之前当你需要更改某些值时,你手动打开了无人参与的 XML 文件。这比你需要做的更多工作——信不信由你,你也可以自动化这一步!

在清单 18-4 中,你正在读取复制的模板文件内容,查找字符串 SQLSVCACCOUNT=, SQLSVCPASSWORD=, 和 SQLSYSADMINACCOUNTS=,并用特定值替换这些字符串。当你完成后,将修改后的字符串写回复制的模板文件。

$configContents = Get-Content -Path $tempFile.FullName -Raw
$configContents = $configContents.Replace('SQLSVCACCOUNT=""', 'SQLSVCACCOUNT="PowerLabUser"')
$configContents = $configContents.Replace('SQLSVCPASSWORD=""', 'SQLSVCPASSWORD="P@$$w0rd12"')
$configContents = $configContents.Replace('SQLSYSADMINACCOUNTS=""', 'SQLSYSADMINACCOUNTS=
"PowerLabUser"')
Set-Content -Path $tempFile.FullName -Value $configContents

清单 18-4:替换字符串

一旦你有了回答文件,并将该文件和 SQL Server ISO 文件复制到即将成为 SQL 服务器的计算机上,安装程序就准备好了:

$copyParams = @{
    Path        = $tempFile.FullName
    Destination = 'C:\'
    ToSession   = $session
}
Copy-Item @copyParams
Remove-Item -Path $tempFile.FullName -ErrorAction Ignore
Copy-Item -Path 'C:\PowerLab\ISOs\en_sql_server_2016_standard_x64_dvd_8701871.iso' 
-Destination 'C:\' -Force -ToSession $session

运行 SQL Server 安装程序

现在你终于准备好安装 SQL Server。清单 18-5 包含了安装 SQL Server 的代码:

$icmParams = @{
    Session      = $session
    ArgumentList = $tempFile.Name
    ScriptBlock  = {
        $image = Mount-DiskImage -ImagePath 'C:\en_sql_server_2016_standard_x64_dvd_8701871
        .iso' -PassThru ❶
        $installerPath = "$(($image | Get-Volume).DriveLetter):"
        $null = & "$installerPath\setup.exe" "/CONFIGURATIONFILE=C:\$($using:tempFile.Name)" ❷
        $image | Dismount-DiskImage ❸
    }
}
Invoke-Command @icmParams

清单 18-5:使用 Invoke-Command 挂载、安装和卸载映像

首先,你在远程机器上挂载复制的 ISO 文件 ❶;然后你执行安装程序,将输出赋值给 $null ❷,因为你不需要它;最后,完成后,你卸载该映像 ❸。在清单 18-5 中,你使用 Invoke-Command 和 PowerShell Direct 来远程执行这些命令。

安装完 SQL Server 后,进行一些清理工作,确保删除所有临时复制的文件,如清单 18-6 所示。

$scriptBlock = { Remove-Item -Path 'C:\en_sql_server_2016_standard_x64_dvd
_8701871.iso', "C:\$($using:tempFile.Name)" -Recurse -ErrorAction Ignore }
Invoke-Command -ScriptBlock $scriptBlock -Session $session
$session | Remove-PSSession

清单 18-6:清理临时文件

到此为止,SQL Server 已经设置完成并准备就绪!仅用 64 行 PowerShell,你就从一个 Hyper-V 主机创建了一个 Microsoft SQL Server。这是一个很大的进展,但你可以做得更好。

自动化 SQL Server

你已经完成了大部分繁重的工作。到目前为止,你已经有了一个可以完成所需操作的脚本。接下来,你需要做的是将所有这些功能整合到 PowerLab 模块中的几个函数里:New-PowerLabSqlServerInstall-PowerLabOperatingSystem 函数。

你将遵循前几章中建立的基本自动化模式:围绕所有常见操作构建函数并调用它们,而不是在许多地方使用硬编码值。最终结果将是一个用户可以调用的单一函数。在清单 18-7 中,你使用现有函数创建虚拟机和 VHD,并创建第二个Install-PowerLabSQLServer函数来存放安装 SQL Server 的代码:

function New-PowerLabSqlServer {
    [CmdletBinding()]
    param
    (

        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [pscredential]$DomainCredential,

        [Parameter(Mandatory)]
        [pscredential]$VMCredential,

        [Parameter()]
        [string]$VMPath = 'C:\PowerLab\VMs',

        [Parameter()]
        [int64]$Memory = 2GB,

        [Parameter()]
        [string]$Switch = 'PowerLab',

        [Parameter()]
        [int]$Generation = 2,

        [Parameter()]
        [string]$DomainName = 'powerlab.local',

        [Parameter()]
        [string]$AnswerFilePath = "C:\Program Files\WindowsPowerShell\Modules\PowerLab
        \SqlServer.ini"
    )

    ## Build the VM
    $vmparams = @{ 
        Name       = $Name
        Path       = $VmPath
        Memory     = $Memory
        Switch     = $Switch
        Generation = $Generation
    }
    New-PowerLabVm @vmParams
    Install-PowerLabOperatingSystem -VmName $Name
    Start-VM -Name $Name
    Wait-Server -Name $Name -Status Online -Credential $VMCredential
    $addParams = @{
        DomainName = $DomainName
        Credential = $DomainCredential
        Restart    = $true
        Force      = $true
 Invoke-Command -VMName $Name -ScriptBlock { Add-Computer @using:addParams } -Credential
    $VMCredential
    Wait-Server -Name $Name -Status Offline -Credential $VMCredential
    Wait-Server -Name $Name -Status Online -Credential $DomainCredential
    $tempFile = Copy-Item -Path $AnswerFilePath
    -Destination "C:\Program Files\WindowsPowerShell\Modules\PowerLab\temp.ini" -PassThru

    Install-PowerLabSqlServer -ComputerName $Name -AnswerFilePath $tempFile.FullName
}

清单 18-7:New-PowerLabSqlServer函数

你应该能识别出大部分代码:这正是我们刚才讲解过的代码,只是现在它被封装成一个函数,便于重用!我使用了相同的代码主体,但不再使用硬编码值,而是将许多属性参数化,使你可以使用不同的参数安装 SQL Server,而无需修改代码本身。

将特定的脚本转化为通用函数可以保留代码的功能性,并在将来你想更改 SQL Server 部署行为时提供更大的灵活性。

让我们来看看清单 18-8 中Install-PowerLabSqlServer代码的重要部分。

function Install-PowerLabSqlServer {
    ❶ param
    (

        [Parameter(Mandatory)]
        [string]$ComputerName,

        [Parameter(Mandatory)]
        [pscredential]$DomainCredential,

        [Parameter(Mandatory)]
        [string]$AnswerFilePath,

        [Parameter()]
        [string]$IsoFilePath = 'C:\PowerLab\ISOs\en_sql_server_2016_standard
        _x64_dvd_8701871.iso'
    )

    try {
        --snip--

     ❷ ## Test to see if SQL Server is already installed
        if (Invoke-Command -Session $session
        -ScriptBlock { Get-Service -Name 'MSSQLSERVER' -ErrorAction Ignore }) {
            Write-Verbose -Message 'SQL Server is already installed'
        } else {

         ❸ PrepareSqlServerInstallConfigFile -Path $AnswerFilePath
 --snip--
    } catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

清单 18-8:Install-PowerLabSqlServer PowerLab 模块函数

你将安装 SQL Server 所需的所有输入类型进行了参数化❶,并添加了错误处理步骤❷来检查 SQL Server 是否已经安装。这使得你可以反复运行该函数;如果 SQL Server 已经安装,函数会直接跳过。

注意,你调用了一个你之前没见过的函数:PrepareSqlServerInstallConfigFile ❸。这是一个辅助函数:一个小函数,捕捉一些你可能会反复使用的功能(辅助函数通常对用户隐藏,并在后台使用)。虽然这不是必须的,但将小块功能拆分出来会使代码更具可读性。一般来说,函数应该只做一件“事”。这里的“事”当然是一个相对的概念,但你编程的越多,你就会有一种直觉,知道什么时候一个函数在做太多事情。

清单 18-9 是PrepareSqlServerInstallConfigFile函数的代码。

function PrepareSqlServerInstallConfigFile {
    [CmdletBinding()]
    param
    (

        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter()]
        [string]$ServiceAccountName = 'PowerLabUser',

        [Parameter()]
        [string]$ServiceAccountPassword = 'P@$$w0rd12',

        [Parameter()]
        [string]$SysAdminAccountName = 'PowerLabUser'
    )

    $configContents = Get-Content -Path $Path -Raw
    $configContents = $configContents.Replace('SQLSVCACCOUNT=""',
    ('SQLSVCACCOUNT="{0}"' -f $ServiceAccountName))
    $configContents = $configContents.Replace('SQLSVCPASSWORD=""',
    ('SQLSVCPASSWORD="{0}"' -f $ServiceAccountPassword))
    $configContents = $configContents.Replace('SQLSYSADMINACCOUNTS=""',
    ('SQLSYSADMINACCOUNTS="{0}"' -f $SysAdminAccountName))
    Set-Content -Path $Path -Value $configContents
}

清单 18-9:PrepareSqlServerInstallConfigFile辅助函数

你会从清单 18-4 中识别到这段代码;它变化不大。你添加了参数PathServiceAccountNameServiceAccountPasswordSysAdminAccountName来表示每个属性,而不是之前使用的硬编码值。

现在,你已经有了所有的函数,启动一个 SQL 服务器仅需几个命令。运行以下代码即可从头开始启动 SQL 服务器!

PS> $vmCred = Import-CliXml -Path 'C:\PowerLab\VMCredential.xml'
PS> $domainCred = Import-CliXml -Path 'C:\PowerLab\DomainCredential.xml'
PS> New-PowerLabSqlServer -Name SQLSRV -DomainCredential $domainCred -VMCredential $vmCred

运行 Pester 测试

又到了该测试的时候了:让我们运行一些 Pester 测试来检验你实施的新更改。在本章中,你在现有的 SQLSRV 虚拟机上安装了 SQL Server。在安装时,你没有做太多配置,并接受了大部分默认安装选项,因此你只需要进行几个 Pester 测试:你需要确保 SQL Server 已经安装,并且确保在安装过程中它读取了你提供的无人值守配置文件。你可以通过验证PowerLabUser是否拥有服务器的 sysadmin 角色,并且 SQL Server 是否以PowerLabUser账户运行来完成这一点:

PS> Invoke-Pester 'C:\PowerShellForSysAdmins\Part II\Creating and Configuring
SQL Servers\Creating and Configuring SQL Servers.Tests.ps1'

Describing SQLSRV
   Context SQL Server installation
    [+] SQL Server is installed 4.33s
   Context SQL Server configuration
    [+] PowerLabUser holds the sysadmin role 275ms
    [+] the MSSQLSERVER is running under the PowerLabUser account 63ms
Tests completed in 6.28s
Passed: 3 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0

一切都通过了检查,所以你可以继续前进了!

总结

在本章中,你终于看到了一个更为具体的 PowerShell 应用示例。在前几章的基础上,你添加了最终的自动化层:在“层叠”在虚拟机上的操作系统上安装软件(SQL Server)。你以类似于前几章的方式进行了操作。你使用一个示例来确定所需的代码;然后,你将这些代码封装成可重用的格式,并将其放入你的 PowerLab 模块中。现在,这一切完成了,你可以通过几行代码创建任意多的 SQL 服务器!

在下一章,你将做一些不同的事情:重新审视你已经写过的代码并进行重构。你将学习最佳编码实践,并确保在添加最终部分之前,你的模块已经处于你需要的状态,这部分内容会出现在第二十章。

第十九章:重构你的代码

图片

在前一章中,你使用现有的虚拟化管理程序、操作系统 ISO 文件和少量代码构建了一个运行 SQL 服务器的虚拟机。这样做意味着你将前几章中创建的多个函数链接在一起。在这里,你将做一些不同的事情:你不再向 PowerLab 模块添加新功能,而是深入研究代码,看看是否可以让你的模块更模块化。

当我说模块化时,我指的是将代码的功能拆分为可重用的函数,这些函数能够处理多种情况。代码越模块化,它的通用性就越强。而代码的通用性越强,它就越有用。通过模块化代码,你可以重用像New-PowerLabVMInstall-PowerLabOperatingSystem这样的函数来安装多种类型的服务器(你将在下一章中看到)。

再看一下New-PowerLabSqlServer

在第十八章中,你创建了两个主要函数:New-PowerLabSqlServerInstall-PowerLabSqlServer。你这样做的目的是为了设置一个 SQL 服务器。但如果你想让你的函数更具通用性呢?毕竟,不同的服务器与 SQL 服务器有很多相同的组件:虚拟机、虚拟磁盘、Windows 操作系统等等。你可以简单地复制你已有的函数,然后将所有特定的 SQL 引用替换为你想要的服务器类型的引用。

但我必须建议你不要这样做。没有必要写那么多额外的代码。相反,你只需要重构现有的代码。重构指的是在不改变功能的情况下,改变代码内部结构;换句话说,重构是为你,程序员,所做的事情。它帮助代码变得更易读,并确保你在扩展项目时不会遇到太多让人头疼的组织问题。

让我们首先看看你创建的New-PowerLabSqlServer函数,见清单 19-1。

function New-PowerLabSqlServer { 
    [CmdletBinding()] 
 ❶ param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 

        [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 

        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 

        [Parameter()] 
        [string]$VMPath = 'C:\PowerLab\VMs', 

        [Parameter()] 
        [int64]$Memory = 4GB, 

        [Parameter()] 
        [string]$Switch = 'PowerLab', 

        [Parameter()] 
        [int]$Generation = 2, 

        [Parameter()] 
        [string]$DomainName = 'powerlab.local', 

        [Parameter()] 
     ❷ [string]$AnswerFilePath = "C:\Program Files\WindowsPowerShell\Modules
           \PowerLab\SqlServer.ini"
    ) 

 ❸ ## Build the VM 
    $vmparams = @{  
        Name       = $Name 
        Path       = $VmPath 
        Memory     = $Memory 
        Switch     = $Switch 
        Generation = $Generation 
    } 
    New-PowerLabVm @vmParams 

    Install-PowerLabOperatingSystem -VmName $Name 
    Start-VM -Name $Name 

    Wait-Server -Name $Name -Status Online -Credential $VMCredential 

  $addParams = @{ 
        DomainName = $DomainName 
        Credential = $DomainCredential 
        Restart    = $true 
        Force      = $true 
    } 
    Invoke-Command -VMName $Name -ScriptBlock { Add-Computer
    @using:addParams } -Credential $VMCredential 

    Wait-Server -Name $Name -Status Offline -Credential $VMCredential 

 ❹ Wait-Server -Name $Name -Status Online -Credential $DomainCredential 

    $tempFile = Copy-Item -Path $AnswerFilePath -Destination "C:\Program
    Files\WindowsPowerShell\Modules\PowerLab\temp.ini" -PassThru 

    Install-PowerLabSqlServer -ComputerName $Name -AnswerFilePath $tempFile
    .FullName -DomainCredential $DomainCredential 
}

清单 19-1: New-PowerLabSqlServer函数

你打算如何重构这段代码?首先,你知道每个服务器都需要一个虚拟机、一个虚拟磁盘和一个操作系统;你在❸和❹之间的代码块中处理了这些需求。

然而,如果你查看这段代码,你会发现你不能简单地将其提取出来并粘贴到一个新函数中。在New-PowerLabSqlServer函数❶中定义的参数在这些行中被使用。请注意,这里唯一特定于 SQL 的参数是AnswerFilePath❷。

现在你已经找出了那些与 SQL 无关的代码,让我们将其提取出来并用它来创建新的函数New-PowerLabServer(清单 19-2)。

function New-PowerLabServer { 
    [CmdletBinding()] 
    param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 

 [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 

        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 

        [Parameter()] 
        [string]$VMPath = 'C:\PowerLab\VMs', 

        [Parameter()] 
        [int64]$Memory = 4GB, 

        [Parameter()] 
        [string]$Switch = 'PowerLab', 

        [Parameter()] 
        [int]$Generation = 2, 

        [Parameter()] 
        [string]$DomainName = 'powerlab.local' 
    ) 

    ## Build the VM 
    $vmparams = @{  
        Name       = $Name 
        Path       = $VmPath 
        Memory     = $Memory 
        Switch     = $Switch 
        Generation = $Generation 
    } 
    New-PowerLabVm @vmParams 

    Install-PowerLabOperatingSystem -VmName $Name 
    Start-VM -Name $Name 

    Wait-Server -Name $Name -Status Online -Credential $VMCredential 

    $addParams = @{ 
        DomainName = $DomainName 
        Credential = $DomainCredential 
        Restart    = $true 
        Force      = $true 
    } 
    Invoke-Command -VMName $Name
    -ScriptBlock { Add-Computer @using:addParams } -Credential $VMCredential 

    Wait-Server -Name $Name -Status Offline -Credential $VMCredential 

    Wait-Server -Name $Name -Status Online -Credential $DomainCredential 
}

清单 19-2: 更通用的New-PowerLabServer函数

此时,你有了一个通用的服务器配置函数,但没有办法指明你要创建的是哪种服务器。让我们通过使用另一个名为ServerType的参数来解决这个问题:

[Parameter(Mandatory)] 
[ValidateSet('SQL', 'Web', 'Generic')] 
[string]$ServerType

注意新的ValidateSet参数。我将在本章稍后深入解释它的作用;现在,你只需要知道的是,它确保用户只能传入此集合中的服务器类型。

现在你有了这个参数,让我们来使用它。在函数的末尾插入一个switch语句,根据用户输入的服务器类型执行不同的代码:

switch ($ServerType) { 
    'Web' { 
        Write-Host 'Web server deployments are not supported at this time' 
        break 
    } 
    'SQL' { 
        $tempFile = Copy-Item -Path $AnswerFilePath -Destination "C:\Program
        Files\WindowsPowerShell\Modules\PowerLab\temp.ini" -PassThru 
        Install-PowerLabSqlServer -ComputerName $Name -AnswerFilePath
        $tempFile.FullName -DomainCredential $DomainCredential 
        break 
    } 
    'Generic' { 
        break 
    } 
 ❶ default { 
        throw "Unrecognized server type: [$_]" 
    } 
}

如你所见,你处理了三种类型的服务器输入(并使用default情况来处理任何异常❶)。但这里有个问题。为了填写 SQL 代码,你从New-PowerLabSqlServer函数中复制并粘贴了代码,而现在你使用了你没有的东西:AnswerFilePath变量。回想一下,当你将通用代码移到新函数时,你将这个变量留下了,这意味着你无法在这里使用它……还是可以吗?

使用参数集

在像前面这样的情况下,当你有一个参数决定需要哪个其他参数时,PowerShell 有一个非常方便的功能叫做参数集。你可以将参数集视为允许你使用条件逻辑来控制用户输入哪些参数。

在这个示例中,你将使用三个参数集:一个用于配置 SQL 服务器,一个用于配置 Web 服务器,以及一个默认集。

你可以通过使用ParameterSetName属性并跟上一个名称来定义参数集。以下是一个示例:

[Parameter(Mandatory)] 
[ValidateSet('SQL', 'Web', 'Generic')] 
[string]$ServerType, 

[Parameter(ParameterSetName = 'SQL')] 
[string]$AnswerFilePath = "C:\Program Files\WindowsPowerShell\Modules\PowerLab\SqlServer.ini", 

[Parameter(ParameterSetName = 'Web')] 
[switch]$NoDefaultWebsite

注意你没有为ServerType分配参数集。未属于任何参数集的参数可以与任何集一起使用。因此,你可以将ServerTypeAnswerFilePath或你将用于 Web 服务器配置的新增参数CreateDefaultWebsite一起使用。

你可以看到这里大部分参数保持不变,但你根据为ServerType传入的值添加了一个最终的参数:

PS> New-PowerLabServer -Name WEBSRV -DomainCredential CredentialHere -VMCredential CredentialHere -ServerType 'Web' -NoDefaultWebsite 
PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere -ServerType 'SQL' -AnswerFilePath 'C:\OverridingTheDefaultPath\SqlServer.ini'

如果你尝试混合并匹配,同时使用两个不同参数集中的参数,你将会失败:

PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere -ServerType 'SQL' -NoDefaultWebsite -AnswerFilePath 'C:\OverridingTheDefaultPath\SqlServer.ini'

New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name SQLSRV -ServerType 'SQL' -NoDefaultWebsite - ... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException 
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer

如果你做相反的操作,既不使用NoDefaultWebsite参数也不使用AnswerFilePath参数,会发生什么呢?

PS> New-PowerLabServer -Name SQLSRV -DomainCredential CredentialHere -VMCredential CredentialHere
-ServerType 'SQL' 
New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name SQLSRV -DomainCredential $credential... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer
PS> New-PowerLabServer -Name WEBSRV -DomainCredential CredentialHere -VMCredential CredentialHere -ServerType 'Web'
New-PowerLabServer : Parameter set cannot be resolved using the specified named parameters. 
At line:1 char:1 
+ New-PowerLabServer -Name WEBSRV -DomainCredential $credential... 
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
    + CategoryInfo          : InvalidArgument: (:) [New-PowerLabServer], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,New-PowerLabServer

你将得到与之前相同的错误,提示无法解析参数集。为什么?因为 PowerShell 不知道该使用哪个参数集!之前我说过你将使用三个集,但你只定义了两个。你需要设置一个默认的参数集。正如你之前看到的,未明确分配给参数集的参数可以与任何集中的参数一起使用。但是,如果你定义了默认的参数集,PowerShell 将在没有任何集参数被使用的情况下使用这些参数。

至于你的默认集,你可以选择定义的 SQL 或 Web 参数集作为默认值,或者你也可以简单地定义一个不特定的参数集,比如 blah blah,这将为所有没有明确定义集的参数创建一个默认集:

[CmdletBinding(DefaultParameterSetName = 'blah blah')]

如果你不想将某个已定义的参数集设置为默认值,可以将其设置为任何值,只要没有使用参数集中的任何参数,PowerShell 将会忽略这两个参数集。在这种情况下,你需要这样做;不使用已定义的参数集是完全可以的,因为你有 ServerType 参数来指示你是否要部署 Web 服务器或 SQL 服务器。

使用你新的参数集,New-PowerLabServer 函数的参数部分看起来像是 清单 19-3。

function New-PowerLabServer { 
    [CmdletBinding(DefaultParameterSetName = 'Generic')] 
    param 
    ( 
        [Parameter(Mandatory)] 
        [string]$Name, 

        [Parameter(Mandatory)] 
        [pscredential]$DomainCredential, 

        [Parameter(Mandatory)] 
        [pscredential]$VMCredential, 

        [Parameter()] 
        [string]$VMPath = 'C:\PowerLab\VMs', 

        [Parameter()] 
        [int64]$Memory = 4GB, 

 [Parameter()] 
        [string]$Switch = 'PowerLab', 
        [Parameter()]
        [int]$Generation = 2, 

        [Parameter()] 
        [string]$DomainName = 'powerlab.local', 

        [Parameter()] 
        [ValidateSet('SQL', 'Web')] 
        [string]$ServerType, 

        [Parameter(ParameterSetName = 'SQL')] 
        [string]$AnswerFilePath = "C:\Program Files\WindowsPowerShell\Modules
        \PowerLab\SqlServer.ini",

        [Parameter(ParameterSetName = 'Web')] 
        [switch]$NoDefaultWebsite 
    )

清单 19-3:新的 New-PowerLabServer 函数

请注意,你有一个对函数 Install-PowerLabSqlServer 的引用。这个函数看起来和将我们带入困境的函数(New-PowerLabSqlServer)相似。不同的是,Install-PowerLabSqlServerNew-PowerLabServer 完成后接管,安装 SQL 服务器软件并进行基本配置。你可能会倾向于对这个函数进行同样的重构。你可以这么做,但一旦你查看 Install-PowerLabSqlServer 中的代码,你会很快意识到,SQL 服务器的安装阶段与其他类型服务器的安装几乎没有共同点。这是一个独特的过程,且很难为其他服务器部署“通用化”。

总结

好吧,现在代码已经很好地重构了,你剩下的是一个可以……提供 SQL 服务器的函数。那么你是不是回到原点了呢?希望不是!即使你没有改变代码的功能,你已经构建了一个基础,方便你在下章中插入创建 Web 服务器的代码。

正如你在本章中看到的,重构 PowerShell 代码并不是一个简单明了的过程。了解如何重构代码,以及在当前情况下哪种方式最适合,是一种通过经验获得的技能。但只要你始终牢记程序员所说的DRY 原则(不要重复自己),你就会走在正确的道路上。最重要的是,遵循 DRY 原则意味着避免重复代码和冗余功能。你在本章中看到了这一点,当你选择创建一个通用函数来创建新服务器,而不是另一个 New-PowerLabInsertServerTypeHereServer 函数时。

你辛苦的工作没有白费。在下一章中,你将重新开始自动化,添加创建 IIS Web 服务器所需的代码。

第二十章:创建与配置 IIS Web 服务器

图片

你已经完成了自动化过程的最后一步:Web 服务器。在本章中,你将使用 IIS,一个内置的 Windows 服务,提供 Web 服务给客户端。IIS 是你在进行 IT 工作时常常遇到的服务器类型——换句话说,它是一个非常适合自动化的领域!与前几章一样,你首先将从零部署一个 IIS Web 服务器;然后你将专注于安装服务并应用一些基本配置。

前提条件

到现在为止,你应该已经熟悉如何创建和设置一个新的虚拟机,因此我们不会再重复这些步骤。我假设你已经有一个安装了 Windows Server 的虚拟机。如果没有,你可以通过运行以下命令,利用我们在 PowerLab 模块中现有的工作:

PS> New-PowerLabServer -ServerType Generic 
-DomainCredential (Import-Clixml -Path C:\PowerLab\DomainCredential.xml)
-VMCredential (Import-Clixml -Path C:\PowerLab\VMCredential.xml) -Name WEBSRV

注意,这次你指定了一个 Generic 服务器类型;这是因为你还没有为 Web 服务器提供完全的支持(这就是本章的任务!)。

安装与设置

创建虚拟机后,就该设置 IIS 了。IIS 是一个 Windows 功能,幸运的是,PowerShell 提供了一个内置命令来安装 Windows 功能,叫做 Add-WindowsFeature。如果你只是做一次性测试,你 可以 使用一行代码来安装 IIS,但既然你正在将这个自动化集成到一个更大的项目中,你将像安装 SQL 一样通过创建一个函数来安装 IIS。我们将其命名为 Install-PowerLabWebServer

你将让这个函数遵循你之前创建的 Install-PowerLabSqlServer 函数的模型。当你开始为这个项目增加更多服务器支持时,你会发现,即使只是为一行代码创建一个函数,也能让使用和修改模块变得更加容易!

最简单的方式是尽可能地模仿 Install-PowerLabSqlServer 函数,去掉任何 SQL Server 特定的代码。通常,我会建议重用现有的函数而不是再创建一个新的,但在这个案例中,你有一个完全不同的“对象”:SQL Server 与 IIS 服务器。拥有一个不同的函数更为合理。在 清单 20-1 中,你只需复制 Install-PowerLabSqlServer 函数,去掉其“核心”部分,同时保留所有公共参数(你需要排除 AnswerFilePathIsoFilePath 参数,因为 IIS 不需要这些参数)。

function Install-PowerLabWebServer {
    param
    (

        [Parameter(Mandatory)]
        [string]$ComputerName,

        [Parameter(Mandatory)]
        [pscredential]$DomainCredential
    )

    $session = New-PSSession -VMName $ComputerName -Credential $DomainCredential

    $session | Remove-PSSession
}

清单 20-1: “框架” Install-PowerLabWebServer 函数

至于如何设置 IIS 服务,那简直是小菜一碟:你只需要运行一个命令来安装 Web-Server 功能。赶紧将这一行添加到你的 Install-PowerLabWebServer 函数中(清单 20-2)。

function Install-PowerLabWebServer {
    param
    (

        [Parameter(Mandatory)]
        [string]$ComputerName,

        [Parameter(Mandatory)]
        [pscredential]$DomainCredential
    )

    $session = New-PSSession -VMName $ComputerName -Credential $DomainCredential

    $null = Invoke-Command -Session $session -ScriptBlock { Add-WindowsFeature -Name 'Web-Server' }

    $session | Remove-PSSession
}

清单 20-2: Install-PowerLabWebServer 函数

你的 Install-PowerLabWebServer 函数的开头部分已经完成!接下来我们添加更多代码。

从零构建 Web 服务器

现在,您已经有了一个 IIS 安装功能,是时候更新您的New-PowerLabServer函数了。回想一下在第十九章中,当您在重构New-PowerLabServer函数时,由于缺乏所需功能,您不得不使用占位符代码来处理 Web 服务器部分。您使用了这一行Write-Host 'Web server deployments are not supported at this time'作为填充代码。现在,让我们将这段文本替换为调用您新创建的Install-PowerLabWebServer函数:

PS> Install-PowerLabWebServer –ComputerName $Name –DomainCredential $DomainCredential

完成此操作后,您可以像处理 SQL 服务器一样启动 Web 服务器!

WebAdministration 模块

一旦 Web 服务器启动并运行,您需要对其进行操作。当Web-Server功能在服务器上启用时,会安装一个名为WebAdministration的 PowerShell 模块。此模块包含了处理 IIS 对象所需的多个命令。Web-Server功能还会创建一个名为 IIS 的 PowerShell 驱动程序,允许您管理常见的 IIS 对象(如网站、应用程序池等)。

PowerShell 驱动程序使您能够像操作文件系统一样浏览数据源。接下来,您将看到,您可以像操作文件和文件夹一样,使用常见的 cmdlet(如Get-ItemSet-ItemRemove-Item)来操作网站、应用程序池以及其他许多 IIS 对象。

要使 IIS 驱动程序可用,您首先需要导入WebAdministration模块。让我们远程连接到您新创建的 Web 服务器,并稍微操作一下该模块,看看您能做些什么。

首先,您将创建一个新的 PowerShell Direct 会话,并以交互模式进入。之前,您主要使用Invoke-Command将命令发送到虚拟机。现在,由于您只是在调查 IIS 的可能性,您使用Enter-PSSession以交互方式在会话中工作:

PS> $session = New-PSSession -VMName WEBSRV 
-Credential (Import-Clixml -Path C:\PowerLab\DomainCredential.xml)
PS> Enter-PSSession -Session $session
[WEBSRV]: PS> Import-Module WebAdministration

注意最终提示符前的[WEBSRV]。这表明您现在正在操作 WEBSRV 主机,并且可以导入WebAdministration模块。一旦模块被导入到会话中,您可以通过运行Get-PSDrive来验证 IIS 驱动程序是否已创建:

[WEBSRV]: PS> Get-PSDrive -Name IIS | Format-Table -AutoSize

Name Used (GB) Free (GB) Provider          Root     CurrentLocation
---- --------- --------- --------          ----     ---------------
IIS                      WebAdministration \\WEBSRV

您可以像使用任何其他 PowerShell 驱动程序一样浏览此驱动程序:通过将其视为文件系统,使用Get-ChildItem列出驱动程序中的项,使用New-Item创建新项,以及使用Set-Item修改项。但执行这些操作并不等于自动化;这只是通过命令行管理 IIS。而您是来进行自动化的!我之所以现在提到 IIS 驱动程序,是因为它在后续的自动化任务中会派上用场,而且了解如何手动操作总是好事,万一自动化出问题,您可以进行故障排除。

网站和应用程序池

WebAdministration模块中的命令几乎可以管理和自动化 IIS 的每个方面。你将首先了解如何处理网站和应用程序,因为网站和应用程序池是系统管理员在现实世界中最常操作的两个常见组件。

网站

你将从一个简单的命令开始:Get-Website,它允许你查询 IIS 并返回当前在 Web 服务器上存在的所有网站:

[WEBSRV]: PS> Get-Website -Name 'Default Web Site'

Name             ID   State      Physical Path                  Bindings
----             --   -----      -------------                  --------
Default Web Site 1    Started    %SystemDrive%\inetpub\wwwroot  http *:80:

你会注意到你已经创建了一个网站。这是因为 IIS 在安装时会有一个名为“Default Web Site”的默认网站。但假设你不想要这个默认网站,而是想创建你自己的网站,你可以通过将Get-Website命令的输出管道传递给Remove-Website来删除它:

[WEBSRV]: PS> Get-Website -Name 'Default Web Site' | Remove-Website
[WEBSRV]: PS> Get-Website
[WEBSRV]: PS>

如果你想创建一个网站,你也可以像使用New-Website命令那样轻松创建一个:

[WEBSRV]: PS> New-Website -Name PowerShellForSysAdmins
-PhysicalPath C:\inetpub\wwwroot\

Name             ID   State      Physical Path                  Bindings
----             --   -----      -------------                  --------
PowerShellForSys 1052 Stopped    C:\inetpub\wwwroot\            http *:80:
Admins           6591

如果网站的绑定有问题,你想要更改它们(比如你想绑定到非标准端口),你可以使用Set-WebBinding命令:

[WEBSRV]: PS> Set-WebBinding -Name 'PowerShellForSysAdmins'
-BindingInformation "*:80:" -PropertyName Port -Value 81
[WEBSRV]: PS> Get-Website -Name PowerShellForSysAdmins

Name             ID   State      Physical Path                  Bindings
----             --   -----      -------------                  --------
PowerShellForSys 1052 Started    C:\inetpub\wwwroot\            http *:81:
Admins           6591
                 05

你已经看到很多关于网站的操作。接下来,我们来看看应用程序池有什么可能性。

应用程序池

应用程序池允许你将应用程序彼此隔离,即使它们运行在同一台服务器上。这样,如果一个应用程序出现错误,它不会影响其他应用程序。

应用程序池的命令与网站的命令类似,正如下面的代码所示。由于我只有一个应用程序池,所以只有DefaultAppPool显示。如果你在自己的 Web 服务器上运行这个命令,可能会看到更多内容:

[WEBSRV]: PS> Get-IISAppPool

Name                 Status       CLR Ver  Pipeline Mode  Start Mode
----                 ------       -------  -------------  ----------
DefaultAppPool       Started      v4.0     Integrated     OnDemand

[WEBSRV]: PS> Get-Command -Name *apppool*

CommandType     Name                              Version    Source
-----------     ----                              -------    ------
Cmdlet          Get-IISAppPool                    1.0.0.0    IISAdministration
Cmdlet          Get-WebAppPoolState               1.0.0.0    WebAdministration
Cmdlet          New-WebAppPool                    1.0.0.0    WebAdministration
Cmdlet          Remove-WebAppPool                 1.0.0.0    WebAdministration
Cmdlet          Restart-WebAppPool                1.0.0.0    WebAdministration
Cmdlet          Start-WebAppPool                  1.0.0.0    WebAdministration
Cmdlet          Stop-WebAppPool                   1.0.0.0    WebAdministration

由于你已经创建了一个网站,接下来我们来看看如何创建应用程序池并将它分配给你的网站。要创建应用程序池,请使用New-WebAppPool命令,如示例 20-3 所示。

[WEBSRV]: PS> New-WebAppPool -Name 'PowerShellForSysAdmins'

Name                     State        Applications
----                     -----        ------------
PowerShellForSysAdmins   Started

示例 20-3:创建应用程序池

不幸的是,并非所有 IIS 任务都有内置的 cmdlet。要将应用程序池分配给现有的网站,你需要使用Set-ItemProperty并更改 IIS 驱动器中的网站❶(如下所示)。要应用该更新,你需要停止❷并重新启动❸该网站。

❶ [WEBSRV]: PS> Set-ItemProperty -Path 'IIS:\Sites\PowerShellForSysAdmins'
   -Name 'ApplicationPool' -Value 'PowerShellForSysAdmins'
❷ [WEBSRV]: PS> Get-Website -Name PowerShellForSysAdmins | Stop-WebSite
❸ [WEBSRV]: PS> Get-Website -Name PowerShellForSysAdmins | Start-WebSite
   [WEBSRV]: PS> Get-Website -Name PowerShellForSysAdmins | 
      Select-Object -Property applicationPool
   applicationPool
   ---------------
   PowerShellForSysAdmins

你还可以看到,你可以通过查看运行Get-Website命令返回的applicationPool属性来确认应用程序池是否已更改。

配置网站的 SSL

现在你已经了解了用于操作 IIS 的命令,接下来我们回到你的 PowerLab 模块,编写一个函数,用来安装 IIS 证书并将绑定更改为端口 443。

你可以从有效的证书颁发机构获取一个“真实”的证书,或者通过使用New-SelfSignedCertificate函数创建一个自签名证书。因为我只是演示这个概念,所以我们现在就创建一个自签名证书并使用它。

首先,编写这个函数,并指定你需要的所有参数(见示例 20-4)。

function New-IISCertificate {
    param(

            [Parameter(Mandatory)]
            [string]$WebServerName,

            [Parameter(Mandatory)]
            [string]$PrivateKeyPassword,

            [Parameter()]
            [string]$CertificateSubject = 'PowerShellForSysAdmins',

            [Parameter()]
            [string]$PublicKeyLocalPath = 'C:\PublicKey.cer',

            [Parameter()]
            [string]$PrivateKeyLocalPath = 'C:\PrivateKey.pfx',

            [Parameter()]
            [string]$CertificateStore = 'Cert:\LocalMachine\My'
    )
    ## The code covered in the following text will go here

}

示例 20-4:New-IISCertificate的开始

这个函数需要做的第一件事是创建一个自签名证书。你可以使用 New-SelfSignedCertificate 命令来完成这项操作,该命令将证书导入本地计算机的 LocalMachine 证书存储 中,所有计算机的证书都存放在这里。当你调用 New-SelfSignedCertificate 时,你可以传递一个 Subject 参数来存储一个字符串,该字符串会告诉你证书的相关信息。生成证书时,它也会被导入到本地计算机中。

列表 20-5 提供了你将用于生成证书的代码行,该代码行使用了传入的主题($CertificateSubject)。记住,你可以使用$null变量来存储命令的结果,这样就不会将任何内容输出到控制台。

$null = New-SelfSignedCertificate -Subject $CertificateSubject

列表 20-5:创建自签名证书

一旦证书被创建,你需要做两件事:获取证书的指纹,并从证书中导出私钥。证书的 指纹 是一个唯一标识证书的字符串;证书的 私钥 用于加密和解密发送到服务器的数据(这里我不详细讲解)。

你本可以从 New-SelfSignedCertificate 的输出中获取指纹,但我们假设这个证书将被用于与创建它的计算机不同的计算机上,因为这更符合实际情况。为了解决这个问题,你需要先从自签名证书中导出公钥,可以使用 Export-Certificate 命令来完成:

$tempLocalCert = Get-ChildItem -Path $CertificateStore | 
    Where-Object {$_.Subject -match $CertificateSubject } 
$null = $tempLocalCert | Export-Certificate -FilePath $PublicKeyLocalPath

上面的命令将给你一个 .cer 公钥文件,你可以使用它,以及一些 .NET 魔法,暂时导入证书并检索指纹:

$certPrint = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$certPrint.Import($PublicKeyLocalPath)
$certThumbprint = $certprint.Thumbprint

现在你有了证书的指纹,你需要导出私钥,你将使用它来绑定到 Web 服务器上的 SSL。以下是导出私钥的命令:

$privKeyPw = ConvertTo-SecureString -String $PrivateKeyPassword -AsPlainText -Force
$null = $tempLocalCert | Export-PfxCertificate -FilePath $PrivateKeyLocalPath -Password $privKeyPw

一旦你有了私钥,就可以使用 Import-PfxCertificate 命令将证书导入到 Web 服务器的证书存储中。然而,在此之前,你需要检查证书是否已被导入。这就是为什么你需要先获取指纹的原因。你可以使用证书的唯一指纹来验证它是否已存在于 Web 服务器上。

要导入你的证书,你需要使用本章前面看到的几个命令:你将创建一个 PowerShell 直接会话,导入 WebAdministration 模块,检查证书是否存在,如果不存在则添加它。你暂时跳过最后一步,并在列表 20-6 中编写代码完成其余的操作。

$session = New-PSSession –VMName $WebServerName 
–Credential (Import-CliXml –Path C:\PowerLab\DomainCredential.xml)

Invoke-Command –Session $session –ScriptBlock {Import-Module –Name
WebAdministration}

if (Invoke-Command –Session $session –ScriptBlock { $using:certThumbprint –in
(Get-ChildItem –Path Cert:\LocalMachine\My).Thumbprint}) {
      Write-Warning –Message 'The Certificate has already been imported.'
} else {
      # Code for importing the certificate
}

列表 20-6:检查证书是否已存在

代码的前两行你应该已经在本章早些时候见过,但请注意,你需要使用 Invoke-Command 来远程导入模块。同样,由于你在 if 语句的脚本块中使用了本地变量,你需要使用 $using: 前缀来扩展远程计算机上的变量。

让我们在 Listing 20-7 中填写 else 语句的代码。你需要做四件事来完成 IIS 证书的设置。首先,你需要将私钥复制到 Web 服务器上。然后,你需要使用 Import-PfxCertificate 导入私钥。最后,你需要设置 SSL 绑定,并强制它使用私钥:

Copy-Item -Path $PrivateKeyLocalPath -Destination 'C:\' -ToSession $session

Invoke-Command -Session $session -ScriptBlock { Import-PfxCertificate 
-FilePath $using:PrivateKeyLocalPath -CertStoreLocation
$using:CertificateStore -Password $using:privKeyPw }

Invoke-Command -Session $session -ScriptBlock { Set-ItemProperty "IIS:\Sites
\PowerShellForSysAdmins" -Name bindings
-Value @{protocol='https';bindingInformation='*:443:*'} }

Invoke-Command -Session $session -ScriptBlock {
    $cert = Get-ChildItem -Path $CertificateStore | 
        Where-Object { $_.Subject -eq "CN=$CertificateSubject" }
    $cert | New-Item 'IIS:\SSLBindings\0.0.0.0!443' 
}

Listing 20-7: 将 SSL 证书绑定到 IIS

需要指出的是,在这段代码中,你将网站的绑定端口设置为 443,而不是 80\。这样做是为了确保网站遵循典型的 SSL 端口 443,允许 Web 浏览器理解你正在加密 Web 流量。

到目前为止,你已经完成了!你已经成功地在 Web 服务器上安装了一个自签名证书,创建了站点的 SSL 绑定,并强制 SSL 绑定使用你的证书!剩下的就是清理你所工作的会话:

$session | Remove-PSSession

在清理会话后,你可以浏览到 https://,并会被提示信任该证书。所有浏览器都会这么做,因为你颁发了一个自签名证书,而不是由公共证书授权机构颁发的证书。信任该证书后,你将看到默认的 IIS 网页。

请务必查看 PowerLab 模块中的 New-IISCertificate 函数,了解如何在一个地方查看所有这些命令。

总结

本章介绍了另一种类型的服务器——Web 服务器。你学习了如何从零开始创建 Web 服务器,方法与创建 SQL 服务器完全相同。你还学习了 WebAdministration 模块中一些命令,该模块随 IIS 一起提供。你了解了如何使用内置命令执行许多基本任务,并查看了创建的 IIS PowerShell 驱动器。为了总结本章内容,你详细跟踪了一个真实的场景,该场景需要将之前涵盖的许多命令和技术结合起来使用。

如果你已经完成了整本书,恭喜你!我们已经覆盖了很多内容,我很高兴你坚持下来了。你学到的技能和你构建的项目应该为你解决 PowerShell 问题打下基础。把你在这里学到的内容带走,合上书本,开始编写脚本吧。只要开始,并用 PowerShell 自动化它。你真正掌握本书中涉及的概念的唯一方法就是练习。现在就是最好的时机!

第二十一章:索引

符号

+(加法运算符),28–29

@(at 符号),27

$(美元符号),14

#(井号),50

|(管道符号),38

..(范围运算符),28

*(通配符字符),10

A

关于主题,9–10

AccountInactive 参数,141

Active Directory (AD) 域,101–102,242–252

ActiveDirectory 模块

Get 命令,139–140

组,145–146

安装,138

对象,139–149

字符串格式化,147

同步进程,149–155

AD 林创建,242–245

临时命令,93

Add() 方法,30,32–33

Add-Computer 命令,256

Add-Content cmdlet,8–9

加法运算符(+),28–29

管理员角色,11,219

别名,4–5

所有用户模块,82

AllSigned 执行策略,42

亚马逊关系型数据库服务(Amazon RDS),188

亚马逊 Web 服务(AWS)

身份验证,174–178

AWS EC2 实例,178–183

弹性 Beanstalk(EB)应用程序,184–188

IAM 最佳实践指南,177

SQL Server 数据库,188–191

应用服务计划,167–168

Append 参数,125,128

应用池,280–281

ArgumentList 参数,95–96

ArrayLists,29–31

数组,26–29

断言,110–111

星号(*),10

@ 符号(@),27

属性,150–151,155

身份验证

亚马逊 Web 服务(AWS),174–178

Microsoft Azure,158–161

PowerShell 远程认证, 101–105

自动错误变量, 66

自动变量, 16–19

AWS EC2 实例, 178–183

Az 模块, 158

Azure SQL 数据库, 168–172

Azure 虚拟机, 161–167

Azure Web 应用, 167–168

B

RAM 银行, 204

bash, 38

最佳实践

不要重复自己 (DRY), 54, 210

命名约定, 220

参数, 236

重构代码, 266–272

二进制模块, 79

布尔值与布尔类型, 12–20

break 关键字, 53

内置前缀, 220

C

强制转换变量, 21, 72

证书存储, 281–282

CimSession 参数, 210–212

类, 23–24

云资源

亚马逊网络服务 (AWS), 174–191

Microsoft Azure, 158–172

cls 命令, 5

cmd.exe 命令, 5, 38

cmdlet

Add-Content, 8–9

ConvertFrom-Json, 132

ForEach-Object, 55–56

与函数对比, 70

Get-Content, 39

Get-Help, 8–9

Get-Member, 25

Get-Module, 80–81

Invoke-RestMethod, 135

Invoke-WebRequest, 134

Measure-Object, 199–201

概述, 6

Search-ADAccount, 141

Test-Connection, 49–50

代码重构, 266–272

COM 对象, 126

命令

临时命令, 93

Add-Computer, 256

cls, 5

cmd.exe 命令, 5, 38

Connect-PSSession, 99–100

ConvertFrom-SecureString, 160

核心命令, 9–10

dir, 5

Disconnect-PSSession, 99

DOS 命令, 4–5

Enable-WsManCredSSP, 103

Enter-PSSession, 98

Export-Csv, 122

Export-Excel, 126

Find-Module, 86–87

Get 命令, 139–140

Get-Alias, 5

Get-Commands, 6–7

Get-PSSession, 99

Get-Service, 37–41

Get-Variable, 16

Get-Vm, 223

help, 8

Import-Csv, 118–121

Import-Excel, 127–128

Invoke-Command, 93–94, 98, 100

Invoke-Pester, 111

New-Object, 33–34

New-PSSession, 97

概览, 4–8

Remove-PSSession, 101

Resolve-DnsName, 124

Select-Object, 24, 128

Set-Variable, 15–16

Start-Service, 38–40

Test-Connection, 123–124

Where-Object, 208

注释, 50

常用参数, 63

社区脚本, 234

比较运算符, 49–50

Compress 参数, 133

ComputerName 参数, 41

条件逻辑, 235

条件语句, 49–54

Connect-AzAccount, 158–160

Connect-PSSession 命令, 99–100

控制台, 4

–contains 运算符, 49

ContainsKey() 方法, 33

context 块, 109

控制流, 48

转换脚本, 233

ConvertFrom-Json cmdlet, 132

ConvertFrom-SecureString 命令, 160

核心命令, 9–10

凭证, 237–238, 243–244

CredSSP, 101–105

CSV 文件, 118–126

大括号, 70

当前用户模块, 82

自定义对象, 33–34

D

数据结构, 26–33

数据类型, 19–23

DelegateComputer 参数, 103–104

分隔符, 118

describe 块, 109

Description 参数, 145

字典, 31

dir 命令, 5

Disconnect-PSSession 命令, 99

文档, 8–10

美元符号 ($), 14

域控制器, 242

不重复自己 (DRY), 54, 210

DOS 命令, 4–5

点表示法, 24

点源, 233

双重跳跃问题, 102–105

Double 类型, 21

双引号与单引号, 22–23

do/whiledo/until 循环, 58–59

动态模块, 79

E

EC2 实例, 178–183

Elastic Beanstalk (EB) 应用程序, 184–188

else 语句, 51

elseif 语句, 51–52

Enabled 条件, 142–143

Enable-WsManCredSSP 命令, 103

加密凭证, 237–238, 243–244

Enter-PSSession 命令, 98

–eq 运算符, 49

错误消息, 6, 19

$Error 变量, 66

ErrorAction 参数, 63–64, 223

$ErrorActionPreference 变量, 64

错误, 62–63

Excel 电子表格, 126–131, 246–247

异常, 62–63

执行策略, 42–43

退出代码, 17–18

扩展, 23

Export-Csv 命令, 122

Export-Excel 命令, 126

外部脚本, 42–46

F

FilePath 参数, 94

Filter 参数, 139–140, 206

Find-Module 命令, 86–87

Finke, Doug, 126

防火墙规则, 170–171

Float 类型,21

浮点数据类型,21

for 循环,57

foreach 循环,54–56,202–203

foreach() 方法,56

ForEach-Object cmdlet,55–56

林创建,242–245

FreeSpace,202

完整会话,96–101

函数

添加参数,71–76

vs. cmdlets,70

辅助函数,262

命名约定,220

概述,6,70–71

管道能力,76–78

G

Gallery,86,108,126

Get 命令,139

Get-Alias 命令,5

Get-Command,6–7

Get-Content cmdlet,39

Get-Help cmdlet,8–9

Get-Member cmdlet,25

Get-Module cmdlet,80–81

Get-PSSession 命令,99

Get-Service 命令,37–41

Get-Variable 命令,16

Get-Vm 命令,223

组,145–146

GroupScope 参数,145

–gt 操作符,49

H

硬盘空间,202

哈希标记(#),50

哈希表,31–33,124

help 命令,8

帮助系统,10

辅助函数,81,262

Hyper-V,218–219

I

IAM 服务,174–178

幂等性,222

Identity 参数,141

if/then 语句,50–51

IIS 网络服务器,275–283

Import-Csv 命令,118–121

Import-Excel 命令,127–128

ImportExcel 模块,126,246–247

基础设施测试,108

实例类选项,190

整数数据类型,20–21

集成脚本环境(ISE),44–45

交互式会话, 98–99

Internet 网关, 179–180

插值, 23

Invoke-Command, 93–94, 98, 100

Invoke-Pester 命令, 111

Invoke-RestMethod 命令, 135

Invoke-WebRequest 命令, 134

IP 地址, 163, 206–208

IPEnabled 属性, 206–208

ISE (集成脚本环境), 44–45

it 块, 110

迭代, 54

J

JavaScript 对象表示法 (JSON) 数据, 131–136

K

Kerberos, 101–102

键值对, 31

L

LABDC 虚拟机

AD-Domain-Services 安装, 242–243

创建, 223–224

$LASTEXITCODE 变量, 17–18

–le 运算符, 49

Length 值, 199–201

换行符, 133

ListAvailable 参数, 81

本地会话, 96

本地变量, 95

本地冗余存储帐户, 164

循环, 54–59, 196

–lt 运算符, 49

M

Mandatory 关键字, 73

Mandatory 参数, 245

$MaximumHistoryCount 变量, 14

Measure-Object 命令, 199–201

成员, 25

组成员, 145

Memory, 203–205

方法, 23, 25–26

Microsoft Active Directory (AD). 参见 ActiveDirectory 模块

Microsoft Azure

身份验证, 158–161

Azure SQL 数据库, 168–172

Azure 虚拟机, 161–167

Azure 网络应用, 167–168

Microsoft.PowerShell.Management 模块, 83

模块化代码, 265

模块容器, 88

模块清单, 84–85, 88, 219–220

模块

Az 模块, 158

组件, 84–85

创建,88–89

自定义模块,86–88

默认,80–83

下载,86–87

Get命令,139

ImportExcel模块,126

导入,82–83

安装,87

PSADSync模块,155

移除,83

卸载,88

N

Name参数,7

命名约定,220

–ne操作符,49

网络信息,205–208

网络堆栈,162–163

New-Object命令,33–34

New-PowerLabSqlServer,266–269

New-PSSession命令,97

非终止错误,62–64

–not操作符,50

NoTypeInformation参数,122–123

$null变量,16–17

O

对象

ActiveDirectory模块,139–149

COM 对象,126

自定义对象,33–34

ForEach-Object cmdlet,55–56

JavaScript 对象表示法(JSON),131–136

Measure-Object cmdlet,199–201

New-Object命令,33–34

概览,23–26

PSCustomObject类型,33–34

Select-Object命令,24,128

Where-Object命令,208

操作系统映像,164–166

操作系统信息,202–203

操作系统安装

加密凭证,237–238

操作系统部署,232–236

Pester 测试,239–240

PowerShell Direct,238–239

先决条件,231–232

SQL 服务器数据库,254–255

组织单位(OUs),148

操作系统部署,232–236

P

param块,72

参数

AccountInactive,141

添加到函数,71–76

Append, 125, 128

ArgumentList, 95–96

属性, 72–74

绑定, 40–41

CimSession, 210–212

常见, 63

Compress, 133

ComputerName, 41

DelegateComputer, 103–104

Description, 145

ErrorAction, 63–64, 223

FilePath, 94

Filter, 139–140, 206

GroupScope, 145

Identity, 141

ListAvailable, 81

Mandatory, 245

Name, 7

NoTypeInformation, 122–123

概述, 6–7

参数集, 269–272

位置参数, 10

Role, 103

ServerType, 269

使用, 236

ValidateSet, 269

WorksheetName, 127

Pester 测试

Active Directory (AD) 域, 250–252

操作系统安装, 239–240

概述, 108–111

PowerLab 模块, 216, 228–229

SQL 服务器部署, 263

ping.exe, 17–18

管道操作符 (|), 38

管道, 38–41, 76–78

数据透视表, 130

位置参数, 10

postcodes.io, 134

PowerLab

安装, 215–216

概述, 213–215

PowerLab 模块

创建, 219–221

Pester 测试, 216, 228–229

前提条件, 218–219

PowerShell Direct, 238–239

PowerShell Gallery, 86, 108, 126

PowerShell 集成脚本环境 (ISE), 44–45

PowerShellGet 模块, 86

偏好变量, 18–19

前缀, 220

私有函数, 81

私钥, 282

process 块, 77–78

提示符, 4

属性, 23–24

PSADSync 模块, 155

PSCustomObject 类型, 33–34

.psm1 文件扩展名, 84

$PSModulePath 环境变量, 82

公共 IP 地址, 163

R

RAM, 204

范围操作符 (..), 28

RBAC (基于角色的访问控制), 175

领域, 101

重构代码, 266–272

远程桌面协议 (RDP) 应用程序, 99

远程服务器管理工具软件包, 138

RemoteSigned 执行策略, 43

Remove() 方法, 25–26, 31

Remove-PSSession 命令, 101

Resolve-DnsName 命令, 124

资源组, 161

REST API, 131, 134–136

受限执行策略, 42

返回代码, 17–18

Role 参数, 103

基于角色的访问控制 (RBAC), 175

路由表, 180

路由, 180

运行空间, 95

S

架构, 150

脚本签名, 44

脚本块, 56, 92–96

脚本

最佳实践, 210

清理与优化, 210–212

输入, 194–195

循环, 196

输出, 194, 196–198

概述, 42–46

远程文件, 199–201

服务器名称, 194–195

Windows 管理工具 (WMI), 201–208

Search-ADAccount cmdlet, 141

安全字符串, 160, 237, 243–244

Select-Object 命令, 24, 128

Server 2012 R2, 94

服务器名称, 194

ServerType 参数, 269

服务主体, 158–160

会话, 96–101

Set-Variable 命令,15–16

SHIFT-TAB,5

should 断言,110

签名数据类型,20

SilentlyContinue 值,223

单引号与双引号,22–23

扩展参数,211

SQL 服务器数据库

亚马逊网络服务 (AWS),188–191

部署,253–263

Microsoft Azure,168–172

方括号 ([]),27

SSL 配置,281–284

Start-Service 命令,38–40

停止条件,54

存储帐户,164

严格模式,15

字符串

ConvertFrom-SecureString 命令,160

概述,21–23

安全字符串,237,243–244

字符串格式化,147

结构化数据

CSV 文件,118–126

Excel 电子表格,126–131

JSON 数据,131–136

子网,162,180–181

switch 语句,52–54,235–236

同步进程,149–155

系统模块,81

T

TAB 补全,5

终止错误,62,64–66

Test-Connection cmdlet,49–50

Test-Connection 命令,123–124

测试。参见 Pester 测试

文本编辑器,44–45

拇指印,282

信任关系策略文档,176

try/catch/finally 结构,64–66

U

无人值守的答案文件,232,255

单元测试,108

不受限制的执行策略,43

无符号数据类型,20

可更新的帮助,10

用户定义变量,14–16

$using 语句,96

V

ValidateSet 参数,269

变量

$Error,66

$ErrorActionPreference,64

$LASTEXITCODE,17–18

$MaximumHistoryCount,14

$null, 16–17

$PSModulePath, 82

$using 语句, 96

扩展, 23

插值, 23

概述, 13–19, 235

值的, 13–15

虚拟环境配置

虚拟硬盘(VHDXs), 225–228

虚拟机(VMs), 223–225

虚拟交换机, 220–223

虚拟硬盘(VHDXs), 225–228

虚拟机(VMs), 223–225, 254

虚拟网络适配器(vNICs), 163

虚拟网络, 162

虚拟专用云(VPCs), 178–179

虚拟交换机, 220–223

VMware, 86–87

W

Web 服务器, 275–283

WebAdministration 模块, 277–281

网站, 278–279

Where-Object 命令, 208

while 循环, 58

通配符字符(*), 10

Windows Management Instrumentation (WMI), 201–208

Windows 查询语言(WQL), 206–207

Windows 远程管理(WinRM)服务, 92

Windows Server 2012 R2, 94

Windows Server 2016 ISO, 231

WorksheetName 参数, 127

PowerShell for Sysadmins 使用了 New Baskerville、Futura、Dogma 和 TheSansMono Condensed 字体。

资源

访问 nostarch.com/powershellsysadmins/ 获取资源、勘误和更多信息。

更多实用的书籍来自 Image NO STARCH PRESS

Image

PENTESTING AZURE APPLICATIONS

测试和保护部署的权威指南

作者 MATT BURROUGH

2018 年 7 月,216 页,$39.95

ISBN 978-1-59327-863-2

Image

实用数据包分析,第三版

使用 Wireshark 解决现实世界的网络问题

作者 CHRIS SANDERS

2017 年 4 月,368 页,$49.95

ISBN 978-1-59327-802-1

Image

绝对 FreeBSD,第三版

The Complete Guide to FreeBSD

作者 MICHAEL W. LUCAS

2018 年 10 月,704 页,$59.95

ISBN 978-1-59327-892-2

Image

Linux 命令行,第二版

完全介绍

作者 WILLIAM SHOTTS

2019 年 3 月,504 页,$39.95

ISBN 978-1-59327-952-3

Image

AUTOTOOLS,第 2 版

GNU Autoconf、Automake 和 Libtool 实践指南

作者: 约翰·卡尔科特(JOHN CALCOTE)

2019 年 11 月,584 页,$49.95

ISBN 978-1-59327-972-1

Image

C++速成课程

快速入门

作者: 乔什·洛斯皮诺索(JOSH LOSPINOSO)

2019 年 9 月,792 页,$59.95

ISBN 978-1-59327-888-5

电话:

1.800.420.7240 或

1.415.863.9900

电子邮件:

SALES@NOSTARCH.COM

网站:

WWW.NOSTARCH.COM

**节省时间。

自动化。**

Image

PowerShell^®既是一种脚本语言,也是一种管理外壳,允许你控制和自动化几乎所有 IT 方面的内容。在PowerShell for Sysadmins中,五次微软^® MVP 获奖者亚当“自动化者”伯特拉姆将向你展示如何使用 PowerShell 管理和自动化桌面和服务器环境,让你能够提前去享受午餐。

你将学到如何:

  • 结合命令、控制流程、处理错误、编写脚本、远程运行脚本,并使用 PowerShell 测试框架 Pester 测试脚本。

  • 解析结构化数据(如 XML 和 JSON),处理常见领域(如 Active Directory、Azure 和 Amazon Web Services),并创建实际的服务器清单脚本

  • 设计并构建一个 PowerShell 模块,展示 PowerShell 不仅仅是关于临时脚本的工具

  • 使用 PowerShell 创建一个完全自动化的 Windows 部署,无需人工干预

  • 从一个 Hyper-V 主机和几个 ISO 文件开始,构建一个完整的 Active Directory 森林

  • 只需几行代码,就能创建无数的 Web 和 SQL 服务器!

全书贯穿的实际案例有助于弥合理论与实际系统之间的差距,作者的趣事使内容更加生动有趣。

停止依赖昂贵的软件和花哨的顾问。学习如何使用PowerShell for Sysadmins管理你自己的环境,让每个人都开心。

关于作者

亚当·伯特拉姆(Adam Bertram)是一位拥有 20 年 IT 经验的资深专业人士,同时也是一位经验丰富的在线商业专家。他是企业家、IT 影响者、微软 MVP、博客作者、培训师、作家和多家技术公司的内容营销作者。亚当还是著名 IT 职业发展平台 TechSnips 的创始人。

涵盖 Windows Powershell v5.1

Image

最棒的极客娱乐™

www.nostarch.com

posted @ 2025-11-30 19:37  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报