てくてくてっく

おそらく技術ブログ

OkHttpのUserAgentにGitのタグバージョンを入れる

OkHttpでリクエストを発行した際のUserAgentはデフォルトで okhttp/4.10.0 のようにクライアントのバージョンが設定されます。
ここにアプリ自体のバージョン情報をのせることができると、サーバー側でのログ調査がスムーズかと思います。

目次

  • OkHttpのUserAgentの設定方法
  • GradleでGitのタグ情報をSystemPropertyに設定
  • SystemPropertyの取得

OkHttpのUserAgentの設定方法

UserAgentヘッダーの設定方法はいくつかあるかと思いますが、今回は下記のようにInterceptorでリクエストを上書きする形をとります。

まずはokhttp3.Interceptor のインターフェースを実装し、リクエストヘッダーを上書きします。

public class UserAgentInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // ヘッダーの上書き 
        Request withHeaderRequest = request.newBuilder()
            .addHeader("User-Agent", "myapp/" + "v0.0.0-dummy")
            .build();

        Response response = chain.proceed(withHeaderRequest);
        return response;
    }
}

作成したInterceptor をOkHttpのクライアント生成時に設定します。

public class OkhttpSample {
    public static void main(String[] args) {
        OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new UserAgentInterceptor()) // Interceptorを追加
            .build();
        Request request = new Request.Builder()
            .url("https://httpbin.org/get")
            .build();
        
        try {
            Response response =  client.newCall(request).execute();
            System.out.println(response.body().string());
        } catch (IOException e) {
            // TODO: handle exception
        }
    }
}

動作確認

上記サンプルでは httpbin.org をURLに設定しており、このエンドポイントはヘッダーの中身を返却してくれます。
下記のようにUserAgentが上書きできていることがわかります。

gradle run

> Task :run
{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "myapp/v0.0.0-dummy", 
    "X-Amzn-Trace-Id": "Root=1-631f59b2-69d9c7ba558ff76856acebdc"
  }, 
  "origin": "*.*.*.*", 
  "url": "https://httpbin.org/get"
}

GradleでGitのタグ情報をSystemPropertyに設定

つづいて、Gitのタグ情報取得についてです。こちらは gradle-git-version というGradleプラグインがありこちらを利用します。

利用は簡単でbuild.gradleに下記のようにプラグインを追加し、

plugins {
    id 'com.palantir.git-version' version '0.15.0'
}

あとは追加される gitVersion() をコールします。今回はrunタスク内でシステムプロパティとして設定してみます。 runの部分は適宜ビルドに用いているタスクに合わせて置き換えてください。

run {
    systemProperties['app.version'] = gitVersion()
}

SystemPropertyの取得とUserAgentの上書き

最後にGitのタグ情報を設定したプロパティを取得し、ヘッダーを上書きします。
今回は lightbend/config を使ってプロパティを読み取りました。この辺りはプロジェクトで普段使っているプロパティ系のライブラリでよいかと思います。 build..gradleに依存を追加し、

dependencies {
    implementation('com.typesafe:config:1.4.2')
}

Interceptorのコンストラクタで読み取りました。

public class UserAgentInterceptor implements Interceptor {
    private static String appVersion;
    public UserAgentInterceptor() {
        Config conf = ConfigFactory.load();
        appVersion = conf.getString("app.version");
    }
    // ...

あとはinterceptのヘッダーを上書きしてあげれば完成です。

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Request withHeaderRequest = request.newBuilder()
            .addHeader("User-Agent", "myapp/" + appVersion) // コンストラクタで設定した値に変更 ☆
            .build();

        Response response = chain.proceed(withHeaderRequest);
        return response;
    }

動作確認

あとは git tag v0.0.3 のようにタグをうち、
Gradleタスクを実行すれば、UserAgentが動的に書き換わっていることが確認できます。

gradle run

> Task :run
{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "myapp/v0.0.3", 
    "X-Amzn-Trace-Id": "Root=1-631f5df8-4723b5e342497eb9317bf8e4"
  }, 
  "origin": "*.*.*.*", 
  "url": "https://httpbin.org/get"
}


BUILD SUCCESSFUL in 4s

コードサンプル

今回紹介したコードはGitHubで公開しています。

github.com

参考文献

vCenterホストの証明書サムプリントをgovcコマンド1つで取得する

vCenter Server TLS 証明書のサムプリントを取得する必要がある際にコマンド1つで楽に実施する方法を紹介します。

もちろんブラウザがあれば vCenter Server の TLS 証明書サムプリントの検索 を参考に数手順でできるのですが、コマンドで取得できた方がメンテナンス手順書を書く際などに都合がよい場合もあるかと思います。

(前提)govcのインストール

https://github.com/vmware/govmomi/blob/master/govc/README.md を参考にインストールします。

証明書の取得

あとはコマンドをたたくだけです。下記のようにサムプリントが表示されます。
-json オプションをつければjsonに成形可能なのでjqコマンド等で必要な情報のみ抽出することも可能です。

$ govc about.cert -u localhost:8989 -k
Certificate Status:          ERROR x509: certificate is valid for example.com, not localhost
Issued To:                   
  Common Name (CN):          <Not Part Of Certificate>
  Organization (O):          Acme Co
  Organizational Unit (OU):  <Not Part Of Certificate>
Issued By:                   
  Common Name (CN):          <Not Part Of Certificate>
  Organization (O):          Acme Co
  Organizational Unit (OU):  <Not Part Of Certificate>
Validity Period:             
  Issued On:                 1970-01-01 00:00:00 +0000 UTC
  Expires On:                2084-01-29 16:00:00 +0000 UTC
Thumbprints:                 
  SHA-256 Thumbprint:        44:8F:62:8A:8A:65:AA:18:56:0E:53:A8:0C:53:AC:B3:8C:51:B4:27:DF:03:34:08:23:49:14:11:47:DC:9B:F6
  SHA-1 Thumbprint:          2C:11:ED:D7:13:87:7D:B5:74:18:B8:1C:42:C2:56:1F:0D:B9:5B:B9

(補足) どのように実装されているのか

govcの実装箇所は下記で、シンプルにcert情報をgolangの標準ライブラリのcrypto/sha256でパースしているようです。

https://github.com/vmware/govmomi/blob/63aa05d35301eb2318c725ee7ac4961d6b7475fd/object/host_certificate_info.go#L50

govmomiでVMのオブジェクトを取得する方法 6パターン

govmomiでオブジェクトの取得方法を探している方のために取得方法をいくつか紹介します。

(前提)govmomi検証環境

govmomiを試すためにはgovmomiのパッケージに含まれるvcsimを使えば、実際のvCenterと疎通しない環境でもコードの実行が可能です。vcsimコマンドにより起動することもできますが、簡単なやり方としてコード内で examples.Run() をすることで内部的にvcsimと疎通したクライアントが取得できるため、これを用いると楽です。

package main

import (
    "context"
    "fmt"

    "github.com/vmware/govmomi/examples"
    "github.com/vmware/govmomi/find"
    "github.com/vmware/govmomi/vim25"
)

func main() {
    examples.Run(func(ctx context.Context, c *vim25.Client) error {
        f := find.NewFinder(c)
        vm, err := f.VirtualMachine(ctx, "DC0_H0_VM0")
        if err != nil {
            return err
        }

        fmt.Printf("name: %s, path: %s, mo-id: %v\n", vm.Name(), vm.InventoryPath, vm.Reference())

        return nil
    })
}

実行結果

> go run main.go
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54

では本題に戻ってVMの取得方法を紹介します。

1. finderでVM名から検索

すでに紹介してしまいましたが、finderで取得する方法です。名前、もしくはパスでVMを検索します。ただし名前で検索する場合は複数一致した場合にfind.MultipleFoundErrorが返ってくるため、複数ある場合を想定するのであれば VirtualMachine 関数ではなく VirtualMachineList 関数の利用が必要です。

examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from name")

        f := find.NewFinder(c)
        vm, err := f.VirtualMachine(ctx, "DC0_H0_VM0")
        if err != nil {
            return err
        }

        fmt.Printf("name: %s, path: %s, mo-id: %v\n", vm.Name(), vm.InventoryPath, vm.Reference())

        return nil
})

実行結果

from name
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54

2. finderでフォルダのパスから検索

特定のフォルダ配下のVMを取得したい場合は、finderでのVM取得方法を少し工夫することで実現可能です。実はfinderの引数にはワールドカードが指定可能なので次のようにフォルダ名以降を * とすることでVM名が不明であってもフォルダ配下のVMとして取得可能です。ちなみに内部的にワイルドカードはGo標準の path.Match が利用されてますので、利用可能な文字や仕様はそちらが参考になります。

examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from folder name")

        f := find.NewFinder(c)
        vms, err := f.VirtualMachineList(ctx, "/DC0/vm/*")
        if err != nil {
            return err
        }

        for _, vm := range vms {
            fmt.Printf("name: %s, path: %s, mo-id: %v\n", vm.Name(), vm.InventoryPath, vm.Reference())
        }

        return nil
})

実行結果

from folder name
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54
name: DC0_H0_VM1, path: /DC0/vm/DC0_H0_VM1, mo-id: VirtualMachine:vm-57
name: DC0_C0_RP0_VM0, path: /DC0/vm/DC0_C0_RP0_VM0, mo-id: VirtualMachine:vm-60
name: DC0_C0_RP0_VM1, path: /DC0/vm/DC0_C0_RP0_VM1, mo-id: VirtualMachine:vm-63

3. mo-idから取得

逆に名前やパスが不明でも、ManagedObjectのIDがわかっていればIDから直接の取得も可能です。finderのObjectReference関数はManagedObjectが存在する場合のみInventoryPathがセットされた状態でオブジェクトが返却されてきます。ただ、返却値がobject.Reference型のためVMのオブジェクトとして利用するためには型アサーションによる変換が必要です。型アサーションはpanicやnilアクセスを起こす原因にもなり得ますので扱いには注意が必要です。

examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from mo-id")

        f := find.NewFinder(c)
        ref, err := f.ObjectReference(ctx, types.ManagedObjectReference{
            Type:  "VirtualMachine",
            Value: "vm-54",
        })
        if err != nil {
            return err
        }
        vm, _ := ref.(*object.VirtualMachine)

        fmt.Printf("name: %s, path: %s, mo-id: %v\n", vm.Name(), vm.InventoryPath, vm.Reference())

        return nil
})

実行結果

from mo-id
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54

4. mo-idから取得(finderを利用しない方法)

先ほどのfinderのObjectReference関数を使った方法は型アサーションが必要であり若干扱いづらそうです。mo-idがわかっているのであれば、object.NewVirtualMachine()関数によりオブジェクト自体を生成してしまう方法も取れます。生成したオブジェクトより、必要な要素をメンバ関数から取得していく方法がとれます。この方法で注意したい点としては、object.NewVirtualMachine関数の返却値にerrorが存在しないことからもわかるように、生成した段階では実際にそのオブジェクトが存在するかは保証されません。そのため生成後に、vm.Name(), vm.InventoryPathなどは空で返却されます。そのためサンプルではvm.ObjectName(ctx)等の関数を利用し、内部的にはプロパティコレクターを利用することで、オブジェクトの存在確認と合わせて情報を収集しています。

examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from mo-id(without finder)")

        vm := object.NewVirtualMachine(c, types.ManagedObjectReference{
            Type:  "VirtualMachine",
            Value: "vm-54",
        })

        name, err := vm.ObjectName(ctx)
        if err != nil {
            return err
        }
        path, err := find.InventoryPath(ctx, c, vm.Reference())
        if err != nil {
            return err
        }

        fmt.Printf("name: %s, path: %s, mo-id: %v\n", name, path, vm.Reference())

        return nil
})

実行結果

from mo-id(without finder)
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54

5. VMのUUIDから取得

VMにはUUIDが存在しますのでこれをベースに取得する方法がシンプルです。SearchIndexのFindByUuid関数により取得します。ポイントとしてはFindByUuid関数はVMとホストが取得可能な関数であったり、UUIDとしてもVM UUIDとInstance UUIDが指定可能な点などオプションの要素がありますのでここを設定する必要があります。オプションの詳細は SearchIndexのAPI仕様 が参考になります。

ちなみにこの記事でSearchIndexというものは初めて登場したようにも見えますが、実はfinderはSearchIndexの FindByInventoryPath と FindChild をラップしたものだったりします。(参照: find/docs.go)

   examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from vm uuid")

        si := object.NewSearchIndex(c)
        ref, err := si.FindByUuid(
            ctx,
            nil, // use default dc
            "265104de-1472-547c-b873-6dc7883fb6cb",
            true,
            nil, // if Instance UUID => types.NewBool(true)
        )
        if err != nil {
            return err
        }

        vm, _ := ref.(*object.VirtualMachine)

        name, err := vm.ObjectName(ctx)
        if err != nil {
            return err
        }
        path, err := find.InventoryPath(ctx, c, vm.Reference())
        if err != nil {
            return err
        }

        fmt.Printf("name: %s, path: %s, mo-id: %v\n", name, path, vm.Reference())

        return nil
})

実行結果

from vm uuid
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54

6. Hostから取得

最後は少し違った方法としてVMを所有するHostから取得します。HostはVMの参照(ManagedObjectReference)を持つため間接的に取得可能です。host.PropertiesでプロパティコレクターによりVMの参照を取得します。ちなみにプロパティコレクターの引数が "vm" であることを特定するためにはManagedObjectの HostSystemの仕様 により vm という子要素を持っていることが確認できるため、引数は "vm" となります。VMの参照が取得できたらすでに紹介したmo-idからの取得方法によりVMのオブジェクトを取得します。

examples.Run(func(ctx context.Context, c *vim25.Client) error {
        fmt.Println("from host")

        f := find.NewFinder(c)
        host, err := f.HostSystem(ctx, "DC0_H0")
        if err != nil {
            return err
        }

        var m mo.HostSystem
        err = host.Properties(ctx, host.Reference(), []string{"vm"}, &m)
        if err != nil {
            return err
        }

        vmrefs := m.Vm
        for _, vmref := range vmrefs {
            vm := object.NewVirtualMachine(c, vmref)

            name, err := vm.ObjectName(ctx)
            if err != nil {
                return err
            }
            path, err := find.InventoryPath(ctx, c, vm.Reference())
            if err != nil {
                return err
            }

            fmt.Printf("name: %s, path: %s, mo-id: %v\n", name, path, vm.Reference())
        }

        return nil
})

実行結果

from host
name: DC0_H0_VM0, path: /DC0/vm/DC0_H0_VM0, mo-id: VirtualMachine:vm-54
name: DC0_H0_VM1, path: /DC0/vm/DC0_H0_VM1, mo-id: VirtualMachine:vm-57

さいごに

govmomiでVMを見つけるための方法をいくつか紹介しました。vCenter関係の自動化やシステム化に取り組もうとしている方の参考になれば幸いです。
コードはGitHubにも公開しましたのでご参照ください。

github.com

VMwareのハンズオンラボでvCLSが無効になっていたので有効化してみた

VCP-DCVの勉強中に、vSphere7.0u1からの新機能として、vCLSというものがあることを知りました。
vSphere クラスタ サービス - VMware Docs

vCLSって?

HAやDRSの機能は従来はvCenter Serverの機能として実装されており、vCenter Server自体の障害が発生した場合に機能しなかったところを、クラスタリングされたVMに管理機能を持たせて可用性を上げる、といった機能のようです。vSphere7.0u1以降であればデフォルトで機能が有効になります。

機能の説明はHoLにもリンクがあった下記の動画がわかりやすかったです。

youtu.be

HoLでは無効化されていた

早速vSphere:最新情報 ハンズオン ラボで触ってみようとしました。しかし、本来はvCLSというフォルダ配下にクラスタリングされたVMが展開されているはずのところ存在しないようです。

f:id:tanopanta:20220306001440p:plain
フォルダ配下のvmが0個

どうやらvCLSを無効化する手段があり、それが適用されているようです。無効化手段は kb に記載があり、クラスターのmo-idを特定しつつ、vCenter構成の詳細設定からそのクラスターのvCLSのRetreateモードを無効にする設定値を追加するという少し裏技的なものでした。

利用したハンズオン ラボの環境ではそもそもDRSやHAの設定自体が無効化されていたので、リソース節約のためにvCLSも無効化されているのかと思います。

有効化してみた

有効化したい場合は無効化する設定の逆をすればよいはずなのでやってみます。

まずは有効化したいvCenterの詳細設定に入ります。

f:id:tanopanta:20220306003847p:plain
vCenterの詳細設定

Edit Settingを押します。

f:id:tanopanta:20220306005505p:plain
edit setting

設定のnameのフィルターで vcls と入力します。

f:id:tanopanta:20220306005617p:plain
フィルターを追加

mo-idがdomain-c1006のクラスターに対してvclsが無効化されていることを確認できます。

f:id:tanopanta:20220306005716p:plain
vclsの設定がfalseになっている

あとはこの設定値をtrueにするか、設定値自体を削除してSaveすれば有効化できます。今回はtrueに変更してみました。

f:id:tanopanta:20220306010158p:plain
trueにしてsave

あとは動作確認です。最初に確認したvCLSフォルダを見てみます。vCLSと名のついたVMが現れました!

f:id:tanopanta:20220306010728p:plain
VMが2つ存在する

試しに電源を落としてみます。

f:id:tanopanta:20220306011053p:plain
電源オフで注意画面が出る

何もしてなくても自動で起動してくるのがわかります。

f:id:tanopanta:20220306011152p:plain
自動で起動する

このようにしてvCLSが無効化されている環境で有効化をすることができました。

いろいろ試してみる

せっかくなのでvCLSの挙動をもう少し見てみます。

vCLSのVMを別のvCenterにvMotionしてみる

f:id:tanopanta:20220306011645p:plain
vCLSが無効化されているvcsa-01bのvcに移行

タスクが失敗しました。これは、宛先のvCenterでvCLSが有効な場合も無効な場合も試しましたがどちらにしても失敗しました。例えば、特定のvCenter配下のVMを全てまとめて移行するようなスクリプトを組んでいる場合は、エラーハンドリング等で少し気にする必要はあるかもしれないですね。

vCLSのVMを消してみた

機能紹介の動画でも実施してましたが、vCLSのVMを消した場合にどうなるかを見てみます。このVMは電源を切った場合にすぐ再起動されるので、削除するには電源オフ直後に削除タスクを発行する必要があります。

削除後の様子が下記です vCLS(3) のように新しい名前でVMが作成されました。

f:id:tanopanta:20220306015403p:plain
削除後

ちなみにどのように作成されたかをタスクで確認してみると、OVFテンプレートがデプロイされ、extraConfig投入後起動されていました。

f:id:tanopanta:20220306015607p:plain
作成タスク