しばたテックブログ

気分で書いている技術ブログです。

PSCoreUpdateというPowerShellモジュールを公開しました

個人的な不便さからPowerShell Coreのアップデートを自動化するためのモジュールを作りました。
ソースと基本的な使い方はGitHubに上げています。

github.com

作った動機

現在、Windows環境においては一度インストールしたPowerShell Core 6.0のアップデートを自動で行う方法は無く、毎回新しいインストーラを手動でダウンロードして実行しなおす必要があります。
一応Office等のMicrosoft製ソフトと同様にMicrosoft Update経由での自動アップデートに対応する計画が立てられている(#6118)のですが、実現までにはまだまだ時間がかかりそうです。

また、MacOS環境においてはHomebrew Caskによる更新が可能ですが、そもそも論としてHomebrewのインストール自体が割と手間なためHomebrewに頼らない更新方法が欲しかったというのもあります。

インストール方法

PowerShell Galleryからインストール可能です。

Install-Module PSCoreUpdate -Scope CurrentUser

なお、このモジュールはあくまで更新のためのツールなので最初にPowerShell Core 6.0をインストールするのは手動で行う必要があります。

使い方

このモジュールには4つのコマンドがあり、各コマンドの使い方をざっくり説明していきます。

Update-PowerShellCore

新しいバージョンのPowerShell Coreがリリースされている場合、インストーラーのダウンロードと自動実行を行います。
Windowsの場合はMSIファイル、MacOSの場合はPKGファイルをダウンロードして実行します。

新しいバージョンが無い場合は何もしませんが、-Forceパラメーターを指定することで過去バージョンのインストールを強制することも可能です。

# 実行例
Update-PowerShellCore -Latest

実行イメージはこんな感じです。

-Silentパラメーターを指定することでサイレントインストールにすることも可能です。

# 実行例
Update-PowerShellCore -Latest -Silent

なお、このコマンドはWindowsとMacOS専用です。
Linux環境においてはAptやYum等の標準のパッケージ管理ツールで十分との判断をしています。

Test-LatestVersion

現在のコンソールが最新バージョンかを判定します。

# 実行例
Test-LatestVersion

最新バージョンの場合

f:id:stknohg:20180408222143p:plain

最新バージョンでない場合

f:id:stknohg:20180408222340p:plain

Find-PowerShellCore

GitHubからPowerShell Coreのリリース情報を取得します。
パラメーターによるバージョン指定も可能です。

# 実行例
Find-PowerShellCore -Latest

f:id:stknohg:20180408222355p:plain

内部的にはGitHub REST APIを使っており、使用回数にレートリミットが設けれられています。
通常であれば問題にならないはずですが、レートリミットに引っかかる様な場合は-Tokenパラメーターにアクセストークンを指定することで回避可能にしています。

# 実行例
Find-PowerShellCore -Latest -Token $env:GITHUB_ACCESS_TOKEN

Save-PowerShellCore

PowerShell Coreのリリースアセット(MSIファイルなど)をダウンロードします。
Download-PowerShellCoreにエイリアスしています。

Save-PowerShellCore -Latest -AssetType MSI_WIN64 -OutDirectory C:\Temp

アセットの種類は以下となっています。

種類 内容
MSI_WIN32 [PowerShell version]-win-x86.msi
MSI_WIN64 [PowerShell version]-win-x64.msi
PKG_OSX1011 [PowerShell version]-osx.10.11-x64.pkg
PKG_OSX1012 [PowerShell version]-osx.10.12-x64.pkg
RPM_RHEL7 [PowerShell version]-rhel.7.x86_64.rpm
DEB_DEBIAN8 [PowerShell version]-debian.8_amd64.deb
DEB_DEBIAN9 [PowerShell version]-debian.9_amd64.deb
DEB_UBUNTU14 [PowerShell version]-ubuntu.14.nn_amd64.deb
DEB_UBUNTU16 [PowerShell version]-ubuntu.16.nn_amd64.deb
DEB_UBUNTU17 [PowerShell version]-ubuntu.17.nn_amd64.deb
APPIMAGE [PowerShell version]-x86_64.AppImage
TAR_LINUXARM32 [PowerShell version]-linux-arm32.tar.gz
TAR_LINUX64 [PowerShell version]-linux-x64.tar.gz
TAR_OSX [PowerShell version]-osx-x64.tar.gz
ZIP_WINARM32 [PowerShell version]-win-arm32.zip
ZIP_WINARM64 [PowerShell version]-win-arm64.zip
ZIP_WIN32 [PowerShell version]-win-x86.zip
ZIP_WIN64 [PowerShell version]-win-x64.zip

【2018/04/23追記】 初期セットアップスクリプト

初期セットアップスクリプトを作りました。
詳しくは下記エントリをご覧ください。

blog.shibata.tech

AutomationNullとは何なのか?

ちょっと前のはなしなのですがこちらのGistを読みました。

こちらのGistではPowerShellを使う上でハマりがちな罠について詳しく記述されておりPowerShellに対する非常に良い批判となっています。

で、このGistの中で$nullAutomationNullの2つのNULLについて触れられています。
私自身AutomationNullの存在は知っていたのですが表に露出しない内部的なものだと認識しており、これまで全然気にかけていませんでした。

本エントリではこのAutomationNullについて分かる範囲で説明したいと思います。

お断り

本エントリの内容はあくまで私個人の調査結果でありPowerShellとして公式な何かを表明するものではありません。

極力間違いの無い様に調べた結果を書いていますが内容に誤りがある可能性は多分にあります。
その点は割り引いてご覧ください。

あと本エントリに対するフィードバックは大歓迎です。
おかしな点などありましたら是非お知らせください。

$nullとは何なのか

現在PowerShellはオープンソースとなっています。
$nullAutomationNullの2つのNULLについて知るにはその実装を見てみるのが一番手っ取り早いでしょう。

最初に$nullについてその実装を見てみます。
PowerShellにおいてNULLを表現する変数である$nullSystem.Management.Automation.NullVariableに定義されています。

/// ShellVariable.cs(Ver.6.0.1) より引用

/// <summary>
/// This class is used for $null.  It always returns null as a value and accepts
/// any value when it is set and throws it away.
/// </summary>
///
internal class NullVariable : PSVariable
{
    /// <summary>
    /// Constructor that calls the base class constructor with name "null" and
    /// value null.
    /// </summary>
    ///
    internal NullVariable() : base(StringLiterals.Null, null, ScopedItemOptions.Constant | ScopedItemOptions.AllScope)
    {
    }

    /// <summary>
    /// Always returns null from get, and always accepts
    /// but ignores the value on set.
    /// </summary>
    public override object Value
    {
        get
        {
            return null;
        }

        set
        {
            // All values are just ignored
        }
    }

    /// <summary>
    /// Gets the description for $null.
    /// </summary>
    public override string Description
    {
        get { return _description ?? (_description = SessionStateStrings.DollarNullDescription); }
        set { /* Do nothing */ }
    }
    private string _description;

    /// <summary>
    /// Gets the scope options for $null which is always None.
    /// </summary>
    public override ScopedItemOptions Options
    {
        get { return ScopedItemOptions.None; }
        set { /* Do nothing */ }
    }
}

Valueプロパティが常にnullを返し、入力はすべて破棄される様になっています。
$nullはPowerShellにおいて.NET Frameworkのnullを表現するものであることがわかります。

ちなみにこの変数のDescriptionには

PS C:\> (Get-Variable 'null').Description
References to the null variable always return the null value. Assignments have no effect.

References to the null variable always return the null value. Assignments have no effect.

と記述されています。

AutomationNullとは何なのか?

そしてAutomationNullSystem.Management.Automation.AutomatinNullに定義されています。

/// AutomationNull.cs(Ver.6.0.1) より引用

/// <summary>
/// This is a singleton object that is used to indicate a void return result.
/// </summary>
/// <remarks>
/// It's a singleton class. Sealed to prevent subclassing. Any operation that
/// returns no actual value should return this object AutomationNull.Value.
/// Anything that evaluates an MSH expression should be prepared to deal
/// with receiving this result and discarding it. When received in an
/// evaluation where a value is required, it should be replaced with null.
/// </remarks>
public static class AutomationNull
{
    #region private_members

    // Private member for Value.

    #endregion private_members

    #region public_property

    /// <summary>
    /// Returns the singleton instance of this object.
    /// </summary>
    public static PSObject Value { get; } = new PSObject();

    #endregion public_property
}

ソースを見る限りいわゆるNULLオブジェクトとして定義されており非常にシンプルな内容です。
実装から見えるものはほとんど無いですが、summaryとremarksタグのコメントに非常に重要なことが書かれています。

AutomationNullはPowerShellにおけるVoidである

summaryコメントにAutomationNullの用途がズバリ書き記されています。

This is a singleton object that is used to indicate a void return result.

indicate a void return resultとある様にAutomationNullはPowerShellにおける式や文を評価した際に戻り値がないことを表すクラスであり、いわゆるVoid型と言われるものに相当します。

そしてremarksコメントに

Any operation that returns no actual value should return this object AutomationNull.Value.
Anything that evaluates an MSH expression should be prepared to deal
with receiving this result and discarding it. When received in an
evaluation where a value is required, it should be replaced with null.

とあり、AutomationNull.Valueを受け取った文や式はその値を切り捨てるべきとされ、値が必要とされる場合はnull(=$null)に置き換えられる様に要請されています。


AutomationNullがVoidであるわかりやすい例として、[void]型(System.Void型)へのキャストはAutomationNull.Valueを返す実装となっている点が挙げられます。

/// LanguagePrimitives.cs(Ver.6.0.1) より一部引用

    public abstract class PSTypeConverter
    {
        // ・・・ (省略) ・・・

        //
        // [void]型へのキャストは AutomationNull.Value を返す様になっている
        //
        private static object ConvertToVoid(object valueToConvert,
                                            Type resultType,
                                            bool recursion,
                                            PSObject originalValueToConvert,
                                            IFormatProvider formatProvider,
                                            TypeTable backupTable)
        {
            typeConversion.WriteLine("returning AutomationNull.Value.");
            return AutomationNull.Value;
        }

        // ・・・ (省略) ・・・

        private static ConversionData FigureLanguageConversion(Type fromType, Type toType,
                                                               out PSConverter<object> valueDependentConversion,
                                                               out ConversionRank valueDependentRank)
        {

            // ・・・ (省略) ・・・

            if (toType == typeof(void))
            {
                return CacheConversion<object>(fromType, toType, LanguagePrimitives.ConvertToVoid, ConversionRank.Language);
            }

            // ・・・ (省略) ・・・
        }

        // ・・・ (省略) ・・・
    }

このほかにも、その実装までは紹介できませんが、AutomationNullはパイプラインに渡されず破棄される、ストリームの出力も無視されるなどの挙動が確認できており、これがAutomationNullがVoidであることの裏付けになっていると私は考えています。

$nullとAutomationNullの違い

ここまででPowerShellに2つのNULLである$nullAutomationNullが存在している理由については明らかにできたかと思います。

これまで私はAutomationNullについてはあくまで内部用で表には露出しないと思っていたのですが、実態は先のGistにある様に当たり前の様に露出します。
(できるだけユーザーに対して見えない様にしていると感じますが、見えないけど確実に存在する状態が現状と思えます)

それどころか、何気なくPowerShellを使い$nullだと思っているもののほとんどが実はAutomationNull.Valueでした。

これは私の予想ですが、PowerShellにおけるNULLはAutomationNullが基本であり、$nullは.NET Frameworkの機能を呼び出すとき等でNULL値を明示する必要がある際に使う特殊な値と認識するのが正解の様です。

ただ、AutomationNullはユーザーに対して見えない様にしたいがために全くドキュメント化されておらず$nullと同じに見える挙動さえします。

この様な現状に対しては正直批判も止む無しと思いますが、直ちにどうにかすることができないのも現実なので割り切りは必要です。

$nullとAutomationNullの挙動の違い

ここから$nullAutomationNullの挙動の違いについて触れます。

原則として、

  • AutomationNullはVoidであり、評価を打ち切るものである
  • $nullは.NET Frameworkのnullを明示するものである

という役割の違いが挙動の違いに表れています。

先述のGistの例を出すと、

# NULL値を明示している
> @($null).Length
1

これはNULL値を明示的に利用する必要があるため要素数1の配列となります。

# AutomationNull.Valueの評価は打ち切られる
> @([System.Management.Automation.Internal.AutomationNull]::Value).Length
0

こちらは配列部分式演算子(@())をAutomationNull.Valueに対して評価することになり、評価を打ち切る必要があるため空の配列となります。

こちらの記述だと違和感があるかもしれませんが、以下の様な記述であれば0を返すことにさほど違和感を感じないのではないかと思います。

# [void]キャストはAutomationNull.Valueを返す
> @([void](Get-Date)).Length
0

補足資料

本エントリでの説明はこれくらいにしておきたいと思います。

AutomationNullの挙動についてはGitHubのIssueにもいくつか挙げられおり、その是非について広く議論されています。
最後にいくつかIssueを紹介しますので興味があればぜひご覧ください。

github.com

github.com

github.com

PowerShell Remoting over SSHを試す - 再び

以前に試した

blog.shibata.tech

ですが、PowerShell Core 6.0が正式リリースされ当時の内容では正しく動作しなくなっていたので改めてやり直しました。

PowerShell Remoting over SSHとは

従来のPowerShell Remoting Protocol(PSRP)では通信の下回りにHTTP/HTTPSを使いますが、PowerShell Remoting over SSHでは名前の通りSSHを使った通信を行います。

基本的な仕組みとしては、

  1. クライアント側はpowershell(.exe)がリモートセッション生成時にssh(.exe)を呼び出し、powershell(.exe)→ssh(.exe)のプロセス間通信を経由してサーバーへの通信を行う。

  2. サーバー側はSSHdのサブシステムにPowerShellを登録。
    クライアントからの接続があった場合はsshd(.exe)→powershell(.exe)とリモートシェルが実行される。

となっています。

PowerShell Remoting over SSHを試す

手順はGitHubで公開されているのでこの手順をベースに行います。
https://github.com/PowerShell/PowerShell/tree/master/demos/SSHRemotinggithub.com

【2018/05/20更新】
いつのまにかGitHubの手順が削除されてしまいました... (#6628)
手順は一致しないかもしれませんが、Docsのこちらの手順が参考になりますので代わりにご覧ください。

もしくは削除直前のこちらのバージョンをご確認ください。

試験環境

試験環境はVirtualBox上でWindows Server 2012 R2とCentOS 7.4.1708を使い双方向で通信させてみます。
他のOSでも細かい部分に違いはあるでしょうが基本的には同じ流れになると思います。

その他細かい条件は以下。

項目 Windows Server 2012 R2 CentOS 7.4.1708
基本設定 インストール直後の状態+最新のWindows Updateを実施 bento/centos7.4のBoxにyum updateを実施
IP 192.168.33.210 192.168.33.209
ユーザー vagrant, administrator vagrant, root

Windows → CentOSへの通信(パスワード認証)

CentOS側の設定

CentOSにPowerShell Coreをインストールして、sshd_configの設定を変更します。
パスワード認証、鍵認証の可否については試験環境のデフォルト設定のままとしました。

# CentOS PowerShell for Linuxのインストール
curl https://packages.microsoft.com/config/rhel/7/prod.repo | sudo tee /etc/yum.repos.d/microsoft.repo
sudo yum install -y powershell

# /etc/ssh/sshd_config の設定
# '# override default of no subsystems'のコメント行の下に設定を追加
# ※位置指定をかなり決め打ちにしているので注意
sudo sed -i.orig -e "/# override default of no subsystems/a Subsystem powershell /usr/bin/pwsh -sshs -NoLogo -NoProfile" /etc/ssh/sshd_config

# sshdの再起動
sudo systemctl restart sshd.service

Windows側の設定

標準でインストールされているPowerShell 4.0ではPowerShell Remoting over SSHできませんので、こちらにもPowerShell Coreをインストールします。
前回と比べてGitHubへの接続にTLS 1.2が必須となったため、Windows PowerShell側の設定を若干変更する必要があります。

# Windows PowerShell 4.0
# GitHubへ接続するためにTLS 1.2を有効化
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

# PowerShell Coreのインストール
Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v6.0.1/PowerShell-6.0.1-win-x64.msi" -OutFile "PowerShell-6.0.1-win-x64.msi"
Start-Process -FilePath msiexec.exe -ArgumentList @("/i", "$($pwd.Path)\PowerShell-6.0.1-win-x64.msi", "/passive") -Wait -PassThru

次にSSHをインストールします。 Zipの解凍をしたいので先ほどインストールたPowerShell Coreから以下の手順でインストールします。

# PowerShell Core 6.0.1
# ssh.exeのインストール 
Invoke-WebRequest -Uri "https://github.com/PowerShell/Win32-OpenSSH/releases/download/v7.6.0.0p1-Beta/OpenSSH-Win64.zip" -OutFile "OpenSSH-Win64.zip"
Expand-Archive -Path ".\OpenSSH-Win64.zip" -DestinationPath "C:\"
[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH') + ';C:\OpenSSH-Win64', 'Machine')

インストール先はC:\OpenSSH-Win64としましたが、こちらについては必要に応じて変更しても構いません。
また、PowerShell Remoting over SSHではssh.exeに対してPATHが通っていること(正確にはssh.exeだけでコマンド呼び出しができること)が必須であるためPATHは必ず通す様にしてください。

テスト接続

ここから実際に試してみます。

まずはNew-PSSessionでPSセッションを作成します。 -HostName(-ComputerNameではない)、-UserNameパラメーターを指定するとPowerShell Remoting over SSHで通信することになります。

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant

実行すると以下の様にssh.exeを使ってサーバーに接続する際のあれやこれやを聞かれるので適切な情報を入力していきます。

f:id:stknohg:20180312193552p:plain

入力後、エラーが出なければ成功です。
$Sessionの中身を確認すると以下の様になり、名前がSSHになっていることがわかります。
このセッションがssh.exeと紐づいています。

f:id:stknohg:20180312193616p:plain

この$sessionを使いEnter-PSSessionしてリモート接続します。

# PowerShell Core
Enter-PSSession -Session $Session

実行結果は以下の様になり、無事CentOSにリモート接続できました。

f:id:stknohg:20180312193640p:plain

リモートからClear-Hostが使えないのは相変わらずでした...
TERM環境変数を適当に設定してみましたが、Clear-Hostしてもエスケープシーケンスが表示されるだけなのでOSが対応しないとダメそうな雰囲気を感じます。
(Windows10やWindows Server 2016だと上手くいくやも?)

最後にRemove-PSSessionしてセッション情報を削除すると紐づいていたssh.exeも終了します。

# PowerShell Core
Remove-PSSession -Session $Session
  
# 個別に消すのが面倒なら以下で一気に消しても良い
Get-PSSession | Remove-PSSession

Windows → CentOSへの通信(鍵認証)

鍵認証で通信する場合、基本的にSSHの鍵認証の方法と同一です。

先ずはWindows側でssh-keygen.exeで鍵を作ります。
鍵の種類やパスフレーズの有無は適当にしてください。

以下の実行例ではRSA2 4096Byte、パスフレーズ有りにしています。

# Windows PowerShell 4.0 / PowerShell Core どちらでも可
#  作成する鍵の設定は適当に
cd C:\OpenSSH-Win64\
.\ssh-keygen.exe -t rsa -b 4096

f:id:stknohg:20180312193705p:plain

あとは作った公開鍵(id_rsa.pub)をCentOS側に転送し~/.ssh/authorized_keysに追加するだけです。 以下の例では/vagrant/id_rsa.pubに鍵を転送しています。

# CentOS bash
# ユーザーはvagrantユーザー
cat /vagrant/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

これで鍵認証の準備は完了です。
鍵認証で接続する場合はNew-PSSession-KeyFilePathパラメーターを使用します。

実行例は以下

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant -KeyFilePath ~\.ssh\id_rsa

f:id:stknohg:20180312193737p:plain

セッションが生成された後はパスワード認証の場合と同様です。

CentOS → Windowsへの通信(パスワード認証)

最後に、CentOSをクライアント、Windowsをサーバーにした場合の接続を試します。
前回同様パスワード認証のみとします。

まずはWindows上でsshdを動作させます。 基本的にはGitHub上のインストール手順に従うだけなので問題ないかと思います。

# Windows PowerShell 4.0
cd C:\OpenSSH-Win64\

# sshdのインストール
.\install-sshd.ps1

# Firewall設定
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

# サービス起動
Start-Service sshd

sshd_configの設定はCentOSの場合と同様でサブシステムにPowerShell(Coreの方)を登録します。
なお、sshd_configC:\ProgramData\ssh\sshd_configを修正してください。(これはVer.1.0.0.0-betaからの変更になります)

追加例 )

# C:\ProgramData\ssh\sshd_config

# override default of no subsystems の欄に追記
Subsystem   sftp    sftp-server.exe
Subsystem powershell c:/program files/powershell/6.0.1/pwsh.exe -sshs -NoLogo -NoProfile

あとパスワード認証も許可しておきます。

# コメント解除
PasswordAuthentication yes

サービスを再起動して設定を反映させます。

Restart-Service sshd

これで準備は完了です。

CentOSからWindowsへ接続してみます。

# CentOS PowerShell Core
$Session = New-PSSession -HostName 192.168.33.210 -UserName administrator

セッション情報はこんな感じになります。

f:id:stknohg:20180312193850p:plain

Enter-PSSessionでWindowsへ接続できます。

# CentOS PowerShell Core
Enter-PSSession -Session $Session

f:id:stknohg:20180312193909p:plain

前回同様にPowerShell Core→PowerShell 4.0(Desktop)とネストしてPowerShellを対話的に実行できないままで、powershell.exe -Command {ほげ}の様にコマンド実行であればネストして実行可能でした。