PowerShellでVMware上の仮想マシンを自動で起動/停止させるスクリプトを書く

普段会社で使っているマシンでVMを動かしているのですが、大体常に起動させています。
OS起動時に立ち上げるような形です。
そこで「どうせいつも起動させてるんだからログオン時にバックグラウンドで自動起動させたいなあ」とふと思い立ちました。
善は急げとも言いますし、今月のネタも特に用意していなかったので、とりあえず書いてみることに。

使っているOSはWindows 7。
その上に VMware WorkStation がインストールされています。
しかし困ったことに、私はWindowsでバッチを組んだ経験が皆無です。
bashなら何度かスクリプトを組んでいますが、Windowsで何かしらのスクリプトを組んだ経験が一切ありません。
ですが、WindowsのCLIに慣れる良い機会かなとも思ったので、わからないなりに勉強しつつやってみようと思いました。
Windows 7ならば、CLI環境は cmd か PowerShell のどちらかになると思いますが、今回はPowerShellで組んでいます。
理由は最近のWindowsOSならば標準搭載されていることと、cmdよりもやれる事が多いと考えたためです。

で、調べつつ書いたので、一旦晒します。
何ぶん初めてなので「もっとこうした方が良いのでは?」と思うところがあればコメントしていただけると幸いです。
そもそも「特定のVMを起動停止させる」という目的のみならば10行満たない程度のスクリプトを書いてしまえば良いと思うのですが、勉強がてら色々詰め込んでいるのでその辺りはご容赦ください。
あと、英語の説明はなんとなくで書いたので、文法とかグダグダだと思います。

# スクリプト本体

```ps1:VMCtlScript.ps1
#####################
# VM Control Script #
#####################

###### - Enviroment Setup - ######
$vmrunDir = "C:\Program Files (x86)\VMware\VMware VIX"
$vmrun = "$vmrunDir\vmrun.exe"
$VMDir = "C:\Users\account\Documents\Virtual Machines"
$VM = Get-ChildItem $VMDir -Recurse -include "*.vmx" -Name | Split-Path -Leaf

###### - Trap - ######
trap {
    $desc = @"
Unexpected error has occurred
This Script interrupted...
"@

    Write-Output $desc

    [System.Windows.Forms.MessageBox]::Show($desc, "Error", "OK", "Error")
}

##########################
###### - Function - ######
##########################

### Exit
function CloseScript($exitcode){
    Stop-Transcript | Out-null
    exit $exitcode
}

### default option Usage
function usage{
    $desc = @"
This Script interrupted...
Please Specify parameters
Usage: vmctld.ps1 <Running Option> [vmx files...]
    <Running Options>
    start   [vmx files]     Boot for virtual machine.
    stop    [vmx files]     Shutdown for virtual machine.
    status                  Display running virtual machine.
"@

    Write-Output $desc

    [System.Windows.Forms.MessageBox]::Show($desc, "Usage", "OK", "information")

    CloseScript 1
}

### vmx file name strings error
function strErr{
    $desc = @"
This vmx file duplicated or not exist.
Please specify vmx file name details.

"@

    if(1 -le $notCtlvmxFile.Length){
        $desc += @"
Invalid strings :
    $notCtlvmxFile

"@
    }

    if(1 -le $dupCtlvmxFile.Length){
        $desc += @"
Duplicate strings :
    $dupCtlvmxFile

"@
    }

    Write-Output $desc
    [System.Windows.Forms.MessageBox]::Show($desc, "Duplicated or Not Exist", "OK", "information")

    CloseScript 2
}

### If specified VM running
function RunningVMs{
        $desc = @"
This specified VM is already runnning.
Please specified other strings.
Running VMs :
$RunVM
"@

    Write-Output $desc
    [System.Windows.Forms.MessageBox]::Show($desc, "already running VMs", "OK", "information")
}

### Running VM judge
function RunVMjudge{
    if(1 -lt $VMlist.Length){
        $RunVM = @()
        for($i = 1; $i -lt $VMlist.Length; $i++){
            for($j = 0; $j -lt $enCtlvmxFile.Length; $j++){
                $RetVal = ($VMlist[$i] -match $enCtlvmxFile[$j])
                if($RetVal -eq "True"){
                    $RunVM += $VMlist[$i]
                }
            }
        }
        if(0 -lt $RunVM.Length){
            RunningVMs
        }
    }
}

### VM action
function param($action,$type){
    if($action -eq "status"){
        & $vmrun list
    }else{
        if($CountParam -gt 1){
            for($i = 0; $i -lt $enCtlvmxFile.Length; $i++){
                $vmxFile = (Get-ChildItem $VMDir -Recurse -Name -include $enCtlvmxFile[$i])
                & $vmrun $action $VMDir\$vmxFile $type
            }
            if(0 -lt $notCtlvmxFile.Length -or 0 -lt $dupCtlvmxFile.Length){
                strErr
            }
        }else{
            usage
        }
        if($null -eq $vmstatus){
            Write-Output @"
$action VM :
$VMDir\$vmxFile
"@
        }
    }
}

#############################
###### - Main Script - ######
#############################
### Output Log file
Start-Transcript C:\Users\account\Documents\VMAutoControlLog\VMControlResult.log | Out-null

Add-Type -Assembly System.Windows.Forms
$CountParam = $args.Length

$enCtlvmxFile = @()
$dupCtlvmxFile = @()
$notCtlvmxFile = @()
for($i = 1; $i -lt $CountParam; $i++){
    $matchVM = ($VM -cmatch $args[$i])
    if(1 -gt $matchVM.Length){
        $notCtlvmxFile += $args[$i]
    }elseif(1 -lt $matchVM.Length){
        $dupCtlvmxFile += $matchVM
    }else{
        $enCtlvmxFile += $matchVM
    }
}

$RetVal = ($null -eq $enCtlvmxFile[0])
if($RetVal -eq "True"){
    $enCtlvmxFile += "><\\"
}

$VMlist = & $vmrun list

### Get Parameter
$runlevel = $args[0]

switch($runlevel){
    status {
        param status
        break
    }
    start {
        $vmstatus = RunVMjudge
        $vmstatus
        param start nogui
        break
    }
    stop {
        param stop soft
        break
    }
    default {
        usage
    }
}

Get-Process vmware -ErrorAction "silentlycontinue"
$RetVal = $?
if($RetVal -eq "True"){
    (Get-Process vmware).CloseMainWindow() 
}

### Output Log file End
CloseScript 0
```

書いていて途中からわけがわからなくなりましたが、きっとそんなもんでしょう。
変数の命名規則とかとっても適当(というかちゃんとした命名規則を知らない)なので、ごっちゃりしてしまいました。
以下、使い方とコードの解説をしていこうと思います。

# 使い方

```ps1:Usage
### VM起動
> .\VMCtlScript.ps1 start CentOS
### VM複数起動
> .\VMCtlScript.ps1 start CentOS Ubuntu

### VM停止(シャットダウン)
> .\VMCtlScript.ps1 start CentOS
### VM複数停止(シャットダウン)
> .\VMCtlScript.ps1 start CentOS Ubuntu

### 起動中のVMの状態表示
> .\VMCtlScript.ps1 status
```

上記のコマンドをログオン/ログオフスクリプトに登録すれば勝手にVMが上がってきます。


# 解説
とりあえず最初っから解説していきましょう。
スクリプト自体も自分用ですし、この解説も完全に自分用なのですが。


## 変数設定

```ps1
#####################
# VM Control Script #
#####################

###### - Enviroment Setup - ######
$vmrunDir = "C:\Program Files (x86)\VMware\VMware VIX"
$vmrun = "$vmrunDir\vmrun.exe"
$VMDir = "C:\Users\account\Documents\Virtual Machines"
$VM = Get-ChildItem $VMDir -Recurse -include "*.vmx" -Name | Split-Path -Leaf
```

\#はコメントです。
まず最初に変数を設定しています。

|変数名|設定しているもの|
|:--|:--|
|\$vmrunDir|vmrun.exe が置いてあるディレクトリを指定している|
|\$vmrun|vmrun.exe を指定している|
|\$VMDir|仮想マシンファイルが置いてあるディレクトリを指定している|
|\$VM|vmxファイルのみを抽出し、格納している|

PowerShellでは、変数に格納する際、必ず頭に\$を付けます。
また、文字や文字列の場合は "(ダブルクォーテーション) で囲みます。
\$vmrunDir、\$vmrun、\$VMDirまでは簡単だと思うのですが、問題は\$VMですね。
こいつは詳しく書かないといけないかも知れません。
今後のために。


### 変数 $VM

```ps1
$VM = Get-ChildItem $VMDir -Recurse -include "*.vmx" -Name | Split-Path -Leaf
```

上記の形ですが、とりあえず一から解説していきましょう。

#### Get-ChildItem $VMDir
`Get-ChildItem` はPoweShellで使えるコマンドレット(要はPowerShellで利用できるコマンド)の一つで、`Get-ChildItem` はファイル一覧を出力してくれるコマンドです。
引数を指定しない場合、カレントディレクトリのファイルを出力してくれます。
今回の場合、 `Get-ChildItem $VMDir` で、\$VMDir を引数として指定しているので、\$VMDir の直下にあるファイルを一覧で出力します。
しかしそれだけでは足りません。
VMwareの場合、 `C:\...\Virtual Machines\VMファイルが保存してあるディレクトリ\VMファイル郡` というディレクトリ構造になっているためです。
今回 \$VM 変数に格納したいのは、VMのメインファイル。つまり.vmxファイルなわけです。
`Get-ChildItem` は指定したディレクトリの直下のファイルまでは出力してくれますが、それ以下の階層のファイルを出力するには別のオプションが必要です。
そこで、更に `-Recurse` オプションを付け足します。

#### -Recurse
`-Recurse` オプションを使うことで、指定したディレクトリ以下のファイルを再帰的に出力してくれます。
実際にコマンドを打つとわかると思いますが、最下層のディレクトリまで全て出力してくれます。
vmxファイルも出力されているのがわかりますね。
では次に、vmxファイルのみを出力します。
そのために更にオプションを付け加えましょう。

#### -include "*.vmx"
`-include` オプションを使うことで、出力を絞り込むことができます。
今回は拡張子.vmxファイルのみ出力させたいので、 `"*.vmx"` を条件に指定します。
ダブルクォーテーションは付けても付けなくてもどちらでも良いようですが、今回は付けています。
これでvmxファイルのみ抽出することができました。
しかし、ここまでのコマンドを実行してみると、ファイル名以外にも余計な情報が出力されてしまっていることがわかります。
具体的には、以下の様な出力になっているかと思われます。

```ps1
> Get-ChildItem $VMDir -Recurse -include *.vmx


    ディレクトリ: C:\Users\account\Documents\Virtual Machines\CentOS7x64


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2015/12/31      5:42       2665 CentOS7x64.vmx


```

PowerShellのコマンドレットは結構お節介で、結構不要な情報が出てきます。
わかりやすくしてくれているのだろうとは思うのですが、特定の情報を変数に格納したい時はなんだかなーと思います。
今回欲しいのはファイル名だけなので、その他の情報は不要です。
そこで更に別のオプションを付け足します。

#### -Name

`-Recurse` オプションを使うことで、不要な情報を取り払い、ファイル名のみで出力させる事ができます。
具体的には以下の様な形です。

```ps1
> Get-ChildItem $VMDir -Recurse -include *.vmx -Name
CentOS7x64\CentOS7x64.vmx
```

ようやくそれっぽくなりました。
しかし、これでもまだ不要な情報があります。
vmxファイルの上のディレクトリ名まで見えてしまっています。
vmxファイルの名前のみ抽出したいので、その上のディレクトリ名は不要です。
しかし、Get-ChildItemにはこれ以上付け足せるオプションはありません。
そこで、パイプを使い出力結果を別のコマンドに渡し、処理をしてもらう事にします。

#### Split-Path -Leaf

パイプで出力結果を `Split-Path` に渡します。
Split-Path もPowerShellのコマンドレットで、引数に応じてパス文字列から特定の文字列を抽出できます。
今回は `-Leaf` を指定していますが、これは末端の要素のみを抽出してくれます。
さて、これを実行してみると以下の様な結果が得られます。

```ps1
> Get-ChildItem $VMDir -Recurse -include *.vmx -Name | Split-Path -Leaf
CentOS7x64.vmx
```

この出力結果を \$VM に格納しています。

## 例外処理

```ps1
###### - Trap - ######
trap {
    $desc = @"
Unexpected error has occurred
This Script interrupted...
"@

    Write-Output $desc

    [System.Windows.Forms.MessageBox]::Show($desc, "Error", "OK", "Error")

    CloseScript 127
}
```

ここでは想定外のエラーが起こった際の例外処理を設定しています。
PowerShellでもbashと同じくtrapがあり、例外処理を実行できます。
bashのtrapについては[この記事](http://qiita.com/ine1127/items/5523b1b674492f14532a)を参考にしてください。

PowerShellでは、 `trap {処理}` という記法で使います。
今回は、\$desc 変数に「スクリプト中断」の旨の説明文を入れます。
その後、 `Write-Output` で、出力するようにしています。
これはログに出力するためです。
次に、少し変わった `[System.Windows.Forms.MessageBox]::Show($desc, "Error", "OK", "Error")` という行があります。
これはメッセージボックスを生成してくれる.NetFrameworkのライブラリ(?)らしいです。
実行されると以下のようなメッセージボックスが表示されます。
![image](https://qiita-image-store.s3.amazonaws.com/0/76791/488d7681-27bb-8c57-d137-5a332ddf41b8.png)

実際に実行するには上記のコードの前にMessageBoxクラスを含んでいるアセンブリをロードする必要があるのですが、それはメインのスクリプトの部分で定義しています。
書式は以下のようになっています。

```ps1
[System.Windows.Forms.MessageBox]::Show(メッセージ本文, メッセージタイトル, ボタン, メッセージアイコン)
```

最後の `CloseScript 127` はスクリプトを終了させるための関数を呼び出しています。
これはこの後に説明します。

## 関数(終了処理)

```ps1
### Exit
function CloseScript($exitcode){
    Stop-Transcript | Out-null
    exit $exitcode
}
```

先ほどの例外処理にも出てきましたが、この後も随所に登場します。
スクリプトを終わらせる関数です。
`CloseScript [Number]` のような形で使います。
引数は(\$exitcode)で回収しています。
中身は簡単な処理しか書いていません。
まず `Stop-Transcript | Out-null` ですが、これはメインのスクリプトで実行したログ出力を停止するためのコマンドレットです。
パイプで `Out-null` に渡しているのは、ログに `Stop-Transcript` を実行した旨の説明が記載されるのを防ぐためです。
次に `exit $exitcode` ですが、これは見て分かる通りexitコマンドで処理を終了させています。
指定している \$exitcode には、関数を実行した時に指定した引数が入ってきます。
これでexitコマンド実行時の終了コードを指定しています。


### 関数(使用法)

```ps1
### default option Usage
function usage{
    $desc = @"
This Script interrupted...
Please Specify parameters
Usage: vmctld.ps1 <Running Option> [vmx files...]
    <Running Options>
    start   [vmx files]     Boot for virtual machine.
    stop    [vmx files]     Shutdown for virtual machine.
    status                  Display running virtual machine.
"@

    Write-Output $desc

    [System.Windows.Forms.MessageBox]::Show($desc, "Usage", "OK", "information")

    CloseScript 1
}
```

使用法を出力してくれる処理です。
引数が足りない時なんかに上記の処理が実行されるようにしています。
大きくは例外処理と同様です。
変数 \$desc に説明文を格納し、 `Write-Output ` で出力し、メッセージボックスとしても表示させ、`CroseScript 1` でCloseScript関数を呼び出し処理を終了させています。

## スクリプト本体(前編)
関数全て説明してから本体に行こうと思ったのですが、どうにも難しいような気がしたので最初に本体からいきます。
解説は2つに分けます

```ps1
#############################
###### - Main Script - ######
#############################
### Output Log file
Start-Transcript C:\Users\account\Documents\VMAutoControlLog\VMControlResult.log | Out-null

Add-Type -Assembly System.Windows.Forms
$CountParam = $args.Length

$enCtlvmxFile = @()
$dupCtlvmxFile = @()
$notCtlvmxFile = @()
for($i = 1; $i -lt $CountParam; $i++){
    $matchVM = ($VM -cmatch $args[$i])
    if(1 -gt $matchVM.Length){
        $notCtlvmxFile += $args[$i]
    }elseif(1 -lt $matchVM.Length){
        $dupCtlvmxFile += $matchVM
    }else{
        $enCtlvmxFile += $matchVM
    }
}

$RetVal = ($null -eq $enCtlvmxFile[0])
if($RetVal -eq "True"){
    $enCtlvmxFile += "><\\"
}

$VMlist = & $vmrun list
```

### 詳細

```ps1
### Output Log file
Start-Transcript C:\Users\account\Documents\VMAutoControlLog\VMControlResult.log | Out-null
```

`Start-Transcript` コマンドレットで、コンソールに表示された内容をログファイルとして保存するようにしています。
パイプで `Out-null` に渡しているのは、`Stop-Transcript` と同様の理由からです。

```ps1
Add-Type -Assembly System.Windows.Forms
```

上記はメッセージボックスを生成する為に必要となるMessageBoxクラスを含んでいるアセンブリをロードしています。

```ps1
$CountParam = $args.Length
```

上記はスクリプト実行時の引数の数を求め、 \$CountParam 変数に格納しています。

```ps1
$enCtlvmxFile = @()
$dupCtlvmxFile = @()
$notCtlvmxFile = @()
for($i = 1; $i -lt $CountParam; $i++){
    $matchVM = ($VM -cmatch $args[$i])
    if(1 -gt $matchVM.Length){
        $notCtlvmxFile += $args[$i]
    }elseif(1 -lt $matchVM.Length){
        $dupCtlvmxFile += $matchVM
    }else{
        $enCtlvmxFile += $matchVM
    }
}
```

上記では、条件によって格納する変数を区別しています。
まず \$enCtlvmxFile, \$dupCtlvmxFile, \$notCtlvmxFile という3つの空の配列を作成しています。
その後for文で、引数の数だけ条件分岐を回します。
条件分岐に入る前に `$matchVM = ($VM -cmatch $args[$i])` で、vmxファイルと引数で部分一致の確認をし、一致していれば \$matchVM 変数に格納します。
一致しない場合は空文字が \$matchVM 変数に格納されます。
条件分岐では、\$matchVM 変数に格納された変数の数毎に、また別の変数に格納します。

|\$matchVM変数の数|格納される変数|変数の意味合い|
|:--:|:--:|:--:|
|0|\$notCtlvmxFile|文字列と一致したvmxファイルが無い場合|
|2以上|\$dupCtlvmxFile|文字列と一致したvmxファイルが複数ある場合|
|1|\$enCtlvmxFile|文字列と一致したvmxファイルが1つある場合|

上記変数は後々使います

```ps1
$RetVal = ($null -eq $enCtlvmxFile[0])
if($RetVal -eq "True"){
    $enCtlvmxFile += "\\"
}
```

上記は \$enCtlvmxFile[0] の変数がnull値だった場合に、"\\" という文字列を格納するif文です。
\$enCtlvmxFile に何かしら入っていないとこの後スクリプトがコケてしまうので、それを防ぐためにわざと格納しています。
"\\" ならば、ファイル名にも利用できない文字列なので、他のvmxファイルに被ることがありません。

```ps1
$VMlist = & $vmrun list
```

上記では、現在起動しているVMのリストを \$VMlist 変数に格納しています。
\$vmrun 変数に格納しているのはvmrun.exeへのフルパスですが、そのままだと実行できません。
前に & を付けると、実行できます。

## スクリプト本体(後編)

```ps1
### Get Parameter
$runlevel = $args[0]

switch($runlevel){
    status {
        param status
        break
    }
    start {
        $vmstatus = RunVMjudge
        $vmstatus
        param start nogui
        break
    }
    stop {
        param stop soft
        break
    }
    default {
        usage
    }
}

Get-Process vmware -ErrorAction "silentlycontinue"
$RetVal = $?
if($RetVal -eq "True"){
    (Get-Process vmware).CloseMainWindow() 
}

### Output Log file End
CloseScript 0
```

\$args はPowerShellの特殊変数で、コマンド実行時の引数を表します。
配列で格納されており、0 が1番目の引数を表します。
その引数を \$runlevel という変数に格納します。
これは単純に \$args[0] だと分かりづらいためです。
今回第一引数には「start,stop,status」のどれかが来るので、switch文でそれぞれにあてはまる処理を書いています。
当てはまらなかった場合は、default が適用され、Usage関数を実行するようになっています。
使用法を表示する関数ですが、後に説明します。

|第一引数|処理|
|:--:|:--:|
|start|VMを起動|
|stop|VMを停止|
|status|起動中のVMを一覧表示|

<br />

```ps1
Get-Process vmware -ErrorAction "silentlycontinue"
$RetVal = $?
if($RetVal -eq "True"){
    (Get-Process vmware).CloseMainWindow() 
}
```
上記はVMwareWorkStationのアプリケーションウィンドウが開いていた場合に、そのウィンドウを閉じる命令です。
`Get-Process` は、起動しているプロセス一覧を表示するコマンドレットです。
今回は引数に vmware を指定しているので、vmware のプロセスがあれば表示します。
ただ、 `Get-Process` は表示が中々うるさかったりするので、 ` -ErrorAction "silentlycontinue"` で出力をさせないようにしています。
`$?` は特殊変数ですが、意味合いはbashと同じで、コマンドの終了コードを返します。
正常終了ならばTrueで、異常ならFalseが返ります。
それを \$RetVal という変数に格納しています。
その \$RetVal をif文に掛けて、戻り値がTrue、つまりプロセスが存在した場合は、 `(Get-Process vmware).CloseMainWindow()` で、ウィンドウを閉じています。

<br />

```ps1
### Output Log file End
CloseScript 0
```
最後まで処理が続行されれば、正常終了という意味で、CloseScript関数を使い終了させます。

簡単ですが、こんな形です。

<br />

さて、関数の説明に行こう・・・と思ったのですが、書いている間になーんかこんがらがってしまいました。
ついでに気力が持たなかったのでここで終了です。

お付き合いいただきありがとうございました。