• PowerShellから Wordを操作する

    テキストファイルをWordに読み込ませ、Wordの文書校正ツールでちょっとした誤字を拾い上げる、ということをやってみた。

    PowerShellのnew-objectはCOMコンポーネントのオブジェクト生成もできちゃうので当然Office絡みの自動化にも使える、と。

    $fn = "utf8.txt"
    $text = [String]::Join("`n", (get-content -encoding UTF8 $fn))

    $word = new-object -comObject "Word.Application"
    $word.Visible = $true

    $doc = $word.Documents.Add()

    $range = $doc.Content
    $range.Text = $text

    $doc.CheckGrammar()

    今までこういうのはJscriptで書いていたんだけども、get-conetntにエンコードを指定できるのが楽ちんでいい(たまたまUTF8なテキストだったので)。対応しているエンコーディングは少ないが、.NET Frameworkのクラスを使って書いてしまえばよい、というのもまたパワフリャ。

    Windows Vista Ultimate x64
    PowerShell 1.0 x64
    Microsoft Word 2007


  • PowerShellから WSUSの更新を自動承認する

    同期される更新全部に対し、受け入れるか否かを検証・判断するのは専属の要員をアサイン出来ないなら無理だな、と思う。
    そういうわけで、一定の更新クラス(セキュリティ問題の修正プログラム、修正プログラム集、重要な更新)に対して自動承認を行っている。

    これらの自動承認とは別に「ルート証明書の更新プログラム」を自動承認したい。

    WSUS APIを参照すると、IUpdateのApproveメソッドで承認できるらしい。
    http://msdn.microsoft.com/en-us/library/aa354567(VS.85).aspx

    ということで↓のスクリプトをPowerShellで書いてみた。
    • 「すべてのコンピュータ」に対して
    • 更新の名前に「ルート証明書の更新プログラム」を含む
    • インストール承認する
    すべてのコンピュータ、を指すIComputerTargetGroupを得る
    $all_computers_group = $update_server.GetComputerTargetGroup([Microsoft.UpdateServices.Administration.ComputerTargetGroupId]::AllComputers)
    $target_computer_group = new-object Microsoft.UpdateServices.Administration.ComputerTargetGroupCollection
    $target_computer_group.Add($all_computers_group) | out-null
    

    更新に対する検索条件。Title以外にも適用されるらしいので、これだと余計なhitがあるかも。。
    $update_scope = new-object Microsoft.UpdateServices.Administration.UpdateScope
    $update_scope.TextIncludes = "ルート証明書の更新プログラム"

    IUpdateを列挙。拒否済み、最新リビジョンでないものはスキップ
    foreach($update in $update_server.GetUpdates($update_scope) | sort ArrivalDate)
    {
    if($update.IsDeclined -or -not $update.IsLatestRevision)
    {
    # 既に拒否済みのもの
    # 最新リビジョンでないもの
    # は相手にしない
    continue
    }

    適用したいコンピュータグループの数だけ承認メソッド呼び出し
      foreach($target_group in $target_computer_group)
    {
    $update.Approve(
    [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,
    $target_group) | out-null
    }



    いつもどおり無保証。どのような結果についても責任を負いかねます。利用は自己責任で。

    trap [Exception] {
    $t = $error[0].ToString().Trim() + "`n" + $error[0].InvocationInfo.PositionMessage.Trim()
    [Diagnostics.EventLog]::WriteEntry("approve_update", $t, "Error", 1)
    }

    [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
    $update_server = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
    $update_server.PreferredCulture ="ja"

    $all_computers_group = $update_server.GetComputerTargetGroup([Microsoft.UpdateServices.Administration.ComputerTargetGroupId]::AllComputers)
    $unassigned_computers_group = $update_server.GetComputerTargetGroup([Microsoft.UpdateServices.Administration.ComputerTargetGroupId]::UnassignedComputers)

    $target_computer_group = new-object Microsoft.UpdateServices.Administration.ComputerTargetGroupCollection
    $target_computer_group.Add($all_computers_group) | out-null

    # 更新に対する条件
    $update_scope = new-object Microsoft.UpdateServices.Administration.UpdateScope
    $update_scope.TextIncludes = "ルート証明書の更新プログラム"


    foreach($update in $update_server.GetUpdates($update_scope) | sort ArrivalDate)
    {
    if($update.IsDeclined -or -not $update.IsLatestRevision)
    {
    # 既に拒否済みのもの
    # 最新リビジョンでないもの
    # は相手にしない
    continue
    }
    $rev_id = $update.Id
    write-host ($update.ArrivalDate.ToLocalTime(), $rev_id.UpdateId, $rev_id.RevisionNumber, $update.Title)
    foreach($app in $update.GetUpdateApprovals() | sort CreationDate)
    {
    $target_group = $app.GetComputerTargetGroup()
    write-host ("`t", $app.CreationDate.ToLocalTime(), $app.Action.ToString(), $target_group.Name)
    }

    foreach($target_group in $target_computer_group)
    {
    $update.Approve(
    [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,
    $target_group) | out-null
    }
    }

    Windows Server 2008
    WSUS 3.0 SP1
    PowerShell 1.0

  • jscriptから WMIを使ってレジストリキー/値列挙

    regコマンド+query操作の方が手っ取り早くはある。。。

    ↓のページに書かれているように、VBScriptだとStdRegProvオブジェクトにEnumKey/EnumValuesなどのメソッドが生えているが、jscriptからだとこれらがない。
     Writing WMI Scripts in JScript (Windows)

    StdRegProvをGetして、
    var locator = new ActiveXObject("WbemScripting.SWbemLocator");
    var server = locator.ConnectServer(computer, "root\\default");
    this.stdregprov = server.Get("StdRegProv");

    呼び出したいメソッド用の入力パラメータを入れる箱のインスタンスを取得して、呼び出したいメソッド用のパラメータ設定をし、
    var in_param = this.stdregprov.Methods_.Item(method_name).InParameters.SpawnInstance_();
    in_param.hDefKey = hkey;
    in_param.sSubKeyName = key;

    StdRegProvにメソッドを呼び出してもらい、
    var out = this.stdregprov.ExecMethod_(method_name, in_param);

    呼び出したメソッドに合わせて結果を取り出す(EnumKeyの例)
    var names = [];
    if(out_param.sNames != null)
    {
    names = out_param.sNames.toArray();
    }
    return names;


    WMIを経由したことでリモートコンピュータに対しても同じコードで処理出来るのはメリットかも。
    しかし、
    •  regコマンドqueryでもリモートコンピュータのキーを列挙できる
    •  リモートコンピュータに対してレジストリ参照するには、当該コンピュータでWindows RPCが動作していて、なおかつWindows Firewallにはじかれないこと
    ので注意。ちまちまと複数のキーを列挙しないといけない状況で外部コマンドをぽこぽこ起動したくない or 一時ファイルを作りたくない、とするならばjscriptで完結するのはいいかもしれない。

    HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall配下を列挙するサンプル。
    例によって無保証です。利用は自己責任で。
    WScript.Echo()しているのでcscript.exeで実行を推奨。

    (function() {

    var Registry = function () {
    this.initialize.apply(this, arguments);
    };

    Registry.prototype = {
    initialize: function(computer) {
    var locator = new ActiveXObject("WbemScripting.SWbemLocator");
    var server = locator.ConnectServer(computer, "root\\default");
    this.stdregprov = server.Get("StdRegProv");

    this.HKCR = 0x80000000; // HKEY_CLASSES_ROOT
    this.HKCU = 0x80000001; // HKEY_CURRENT_USER
    this.HKLM = 0x80000002; // HKEY_LOCAL_MACHINE
    this.HKUS = 0x80000003; // HKEY_USERS
    this.HKCC = 0x80000005; // HKEY_CURRENT_CONFIG

    this.REG_SZ = 1;
    this.REG_EXPAND_SZ = 2;
    this.REG_BINARY = 3;
    this.REG_DWORD = 4;
    this.REG_MULTI_SZ = 7;
    },

    do_method: function(method_name, hkey, key, value_name) {
    var in_param = this.stdregprov.Methods_.Item(method_name).InParameters.SpawnInstance_();
    in_param.hDefKey = hkey;
    in_param.sSubKeyName = key;
    if(value_name != null)
    {
    in_param.sValueName = value_name;
    }
    var out = this.stdregprov.ExecMethod_(method_name, in_param);
    return out;
    },

    EnumKey: function(hkey, key) {
    var out_param = this.do_method("EnumKey", hkey, key);
    var names = [];
    if(out_param.sNames != null)
    {
    names = out_param.sNames.toArray();
    }
    return names;
    },

    EnumValues: function(hkey, key) {
    var out_param = this.do_method("EnumValues", hkey, key);
    var value_names = [];
    if(out_param.sNames != null)
    {
    value_names = out_param.sNames.toArray();
    }
    var value_types = [];
    if(out_param.Types != null)
    {
    value_types = out_param.Types.toArray();
    }

    return {
    Names: value_names,
    Types: value_types
    };
    },

    GetStringValue: function(hkey, key, name) {
    // REG_SZ
    var out_param = this.do_method("GetStringValue", hkey, key, name);

    // 値が存在しない場合null
    return out_param.sValue;
    },

    GetExpandedStringValue: function(hkey, key, name) {
    // REG_EXPAND_SZ
    var out_param = this.do_method("GetExpandedStringValue", hkey, key, name);

    // 値が存在しない場合null
    return out_param.sValue;
    },

    GetDWORDValue: function(hkey, key, name) {
    // REG_DWORD
    var out_param = this.do_method("GetDWORDValue", hkey, key, name);

    // 値が存在しない場合null
    return out_param.uValue;
    }
    };

    var enum_installs = function(reg, base_key)
    {
    var names = reg.EnumKey(reg.HKLM, base_key);
    for(var i = 0 ; i < names.length; i++)
    {
    WScript.Echo(names[i]);

    var sub_key = base_key + "\\" + names[i];
    var values = reg.EnumValues(reg.HKLM, sub_key);

    var value_names = values.Names;
    var value_types = values.Types;

    for(var j = 0 ; j < value_names.length; j++)
    {
    var value_name = value_names[j];
    var value_type = value_types[j];
    var val = "";
    switch(value_type)
    {
    case reg.REG_SZ:
    val = reg.GetStringValue(reg.HKLM, sub_key, value_name);
    break;
    case reg.REG_EXPAND_SZ:
    val = reg.GetExpandedStringValue(reg.HKLM, sub_key, value_name);
    break;
    case reg.REG_DWORD:
    val = reg.GetDWORDValue(reg.HKLM, sub_key, value_name);
    break;
    }

    if(val == null)
    {
    val = "";
    }

    // 改行をつぶしちゃう
    val = val.toString().replace(/[\x0a\x0d]/g, " ");
    WScript.Echo(["", value_name, value_type, val].join("\t"));
    }
    }

    };

    var computer = WScript.Arguments.Named("computer");
    if(!computer)
    {
    computer = ".";
    }

    var reg = new Registry(computer);

    enum_installs(reg, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
    enum_installs(reg, "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"); })();

    環境:
    Windows XP Professional SP3
    Windows Vista Ultimate SP2 x64
    Windows Server 2008 SE x64

  • PowerShellから WSUS APIを使う

    前提条件とやりたいこと
    • WSUSでWindows PCの更新管理をしている。
    • ここの運用では、ほとんどのコンピュータはADドメインに参加しており、コンピュータアカウントの管理者が設定してある。
    • 一定の間隔で更新未適用コンピュータの一覧を作成し自動的にメール通知するようにしたい。
      • 更新クラスが「SP、セキュリティ問題の修正プログラム、修正プログラム集、重要な更新」のいずれかで、これらの更新をインストール完了していないコンピュータの一覧。
      • メールの内容を自由にカスタマイズしたい。
    • また、当該コンピュータの管理者の氏名を一覧に含めることで連絡リストを兼ねたい。
      • WSUSで得たコンピュータ名をキーにActive Directoryに問い合わせ、管理者名を付記する。
      • ここの環境ではWSUSサーバとDCは同じサーバで運用。
    • メールの送付に関する設定および送付先はWSUSの通知設定と同じ設定を用いる
      • 「状態レポート」の通知先に送る。(更新レポートの通知先でなく)

    WSUSの機能は.NET Frameworkから利用することが出来る。
    http://msdn.microsoft.com/en-us/library/aa354567(VS.85).aspx

    PowerShellは.NETのクラスライブラリを使うことが出来るから、WSUSの操作をスクリプトから行うことが出来る。

    ということで、レポートスクリプト
    無保証です。使用は自己責任で。
    タスクスケジューラで定期的に実行している。ここの環境はw2k8のため管理者としての実行が必要だった。

    .NETのクラスライブラリ(に限らずCOMでもActiveXでも)がスクリプトからさくさく使えるのはなんとも強力。unixのtarやgzipやsha1sumのようなレベルのコマンドがないので手間がかかるな、と感じることはあるけど、書き下ろせなくはない強力さがある。

    アセンブリのロードと各種情報(更新クラスのTitleとか)の日本語化指定
    [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
    $update_server = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
    $update_server.PreferredCulture ="ja"

    IComputerTarget.FullDomainNameからコンピュータアカウントを検索
    $searcher = New-Object DirectoryServices.DirectorySearcher
        $manage_cn = ""
        $computer_account = $computer.FullDomainName.Split(".")[0] + '$'
        $searcher.Filter = "(sAMAccountName=$computer_account)"
        $result = $searcher.FindOne()
        if($result -ne $null)
        {
            $managed_by = $result.Properties["managedby"]
    
            if($managed_by -match "CN=(?<cn>[^,]+)")
            {
                $manage_cn = $matches.cn
            }
        }
    ※$computerはIComputerTarget

    メール作成と送信
    $email_conf = $update_server.GetEmailNotificationConfiguration()
    $mail = New-Object System.Net.Mail.SmtpClient($email_conf.SmtpHostName)
    $mail.Timeout = 30 * 1000

    $msg = New-Object System.Net.Mail.MailMessage
    $msg.Subject = "[WSUS]更新未適用コンピュータ"
    $msg.From = New-Object System.Net.Mail.MailAddress($email_conf.SenderEmailAddress, $email_conf.SenderDisplayName)
    foreach($rcpt in $email_conf.StatusNotificationRecipients)
    {
    $msg.To.Add($rcpt)
    }
    $msg.Body = $body

    $mail.Send($msg)
    ※デフォルトで本文charsetおよびsubjectのMIMEエンコードのcharsetはutf8。
    ※WSUS側で通知設定がされていない場合は想定せず。
    ※ここの環境ではSMTP認証が不要のため無視している。


    環境:
      Windows Server 2008(DC)
      WSUS 3.0 SP1
      PowerShell 1.0

  • squidと ftpと selinux

    squidを使ってプロクシサーバをたてた。
    NATの内側にproxyがある。

    http/httpsは問題ないが、LAN内のブラウザ→squid→ftpリソース、だと取得できない。
    試しにsquidが動作しているサーバでwgetを使って同じftpリソースを取得してみたがこっちはOK。なので、ネットワーク的にはいけるはず。

    PASVになってないのかな?と思ってtsharkでキャプチャしてみると、PASVに対して肯定応答をもらっているのに、その後なぜかPORTを送り、相手のサーバからConnection確立できない、という応答。

    今度はstraceで様子を見てみると、PASVを送ったあと相手のサーバに接続しようとしたところでconnectがEACCESでエラーになっている。

    どういうこと?と思ったけど「squid ftp selinux」で検索してみると良くある話題のようだ。
    selinuxポリシー内で、squidがftp接続に行けるポートを制限しているらしい。

    selinuxってこういう制限もするのか。

    実のところ、selinuxでブロックされている、ということは後からわかった。
    試行錯誤しまくった過程で、iptablesのモジュール(/etc/sysconfig/iptables-configのIPTABLES_MODULES)にip_conntrack_ftp を加えても通るようになった。これはいまだになぜかわかってない・・・。


    CentOS:5.3
    squid-2.6.STABLE21-3.el5
    selinux-policy-2.4.6-203.el5
    selinux-policy-targeted-2.4.6-203.el5