砂漠の旅人(たびと)

UNIX / MS-DOS 時代から電脳砂漠を旅しています

【JSON 編集】Redfish のような入れ子の JSON 群を一つの JSON にマージしたい

こんにちは、たびとです。

仕事で Redfish を使っていますが、@odata.id に定義された子のURL を たどって情報を収集する必要がありますが、 Postman や curl を使っても、2 桁を超えるとイヤになります。

この入れ子構造の JSON ツリーを枝から枝へとたどりながら、 葉までの情報をすべて収集し、一つの JSON にするための アプリがあると便利だろうなと思いました。

本来は、Redfish が使えるサーバで実施したいのですが、 Redfish を利用できる環境を用意するのはハードルが高いと思われます。 そこで、簡単に試せるように、Redfish サイトのサンプル JSON をファイル化して、 複数の JSON ファイルをマージするプログラムを作ってみたいと思います。

この記事の対象者

  • Redfish のような JSON入れ子構造をアプリで開発してみたい方
  • JSON 情報を取得するだけでなく、思い通りに編集してみたい方

Redfish とは

簡単に言うと、Redfish とは REST API を使って JSON でサーバ情報を取得する仕組みです。 従来は、IPMI によりサーバの情報を取得していましたが、 IPMItool のパラメータが面倒なので、個人的には Redfish の方がよいと思います。

Redfish の詳細については、以下を参照してください。

www.dmtf.org

Redfish で取得できるデータ

Redfish モックアップサイトを見ると、どんな情報が取得できるのかが理解できると思います。

redfish.dmtf.org

このサイトの Simple Rack-mounted Server をクリックすると、 以下の画面が表示されます。

Redfish のトップに表示される情報

この JSON 中に定義された @odata.id が子の情報としてあり、 例えば、”redfish/v1/Systems" をクリックすると、Systems の情報が表示されます。

Systems の情報

Systems 中に定義された @odata.id の "/redfish/v1/Systems/437XR1138R2" をクリックすると、 さらに子の情報が表示されるといった入れ子型の階層構造になっています。

今回用意するデータ

実際のサーバに REST API でアクセスするプログラムを作りたかったのですが、 それは難しそうなので、今回はモックアップのデータを JSON ファイルに置き換えて試してみます。

名前は、トップに表示された内容を Main.json とし、子要素の名前は、@odata.id に定義されてる /redfish/v1/xxx/yyy" から "/redfish/v1/" を除く文字列を '/' の代わりに '_' に置き換えた名前にします。 また、先頭行(例:redfish » v1)の行は削除して、'{' に変更し、最終行に '}' を追加して JSON として正しい状態にします。

入れ子構造の JSON をマージさせる

まずは、Visual Studio 2022 で .NET 6 のコンソールアプリ(C#)を適当な名前で作成します。 JSON で編集するために、定番の Newtonsoft.Json を Nuget で追加します。 これは、JsonTextReader と JsonTextWriter を使った方が、JSON を編集するのに楽だからです。

JsonTextReader

JSON を読み込んで、Json.NET の要素に分解してくれます。

詳細は、以下の Json.NET のサンプルを参照してください。

www.newtonsoft.com

Json.NET では JSON の要素を JsonToken として、以下のように扱います。

JsonToken JSON
JsonToken.StartObject {
JsonToken.EndObject }
JsonToken.StartArray [
JsonToken.EndArray ]
JsonToken.PropertyName プロパティ名

JsonTextWriter

Jon.NET の要素から JSON を作成します。 要するに、JsonTextReader と逆のことをします。

詳細は、以下の Json.NET のサンプルを参照してください。

www.newtonsoft.com

マージさせる方法

JsonTextReader を使って、@odata.id が出現するたびに再帰を使って 子の JSON を読み込みます。 このとき、JsonTextReader で読み込んだ Json.NET の要素を List<> に格納していき、 最後に List<> に格納した Json.NET の要素を JsonTextWriter を使って出力するだけです。

このとき、出現した @odata.id の子要素は、"/* @odata.child */" プロパティ配下に追加します。

    "Systems": {
        "@odata.id": "/redfish/v1/Systems",
        "/* @odata.child */": {
            /* ここに Systems.json の内容を追加する */
        }
    },

それと、再帰の注意点は、最終行に自分自身の @odata.id が存在することです。 このため、何も対策せずに再帰すると、自分自身への再帰を繰り返し、スタックオーバーフローになります。 今回のプログラムでは 1 度出現した @odata.id は再帰しないようにしました。

また、@odata.id に対応する JSON ファイルの一部しか用意していないため、 JSON ファイルがない場合、"/* @odata.child */": null と表示するようにしました。

マージさせるコード

Json.NET の要素を格納するクラスを JsonElement とします。 JSON は「"Property”: Value」の形式をとるので、 JsonLine クラスに左右の値を格納できるようにします。 これを Redfish クラスの List再帰しながら格納するように設計しました。

using Newtonsoft.Json;
using System.Text;

var redfish = new Redfish { Root = @"D:\work\RedfishMockup" };
redfish.Read("main.json");
redfish.Write();

/// <summary>
/// JSONの要素
/// </summary>
class JsonElement
{
    public JsonToken TokenType { get; set; }
    public object? Value { get; set; }
}

/// <summary>
/// Redfishマージ用
/// </summary>
class Redfish
{
    public string Root { get; set; } = string.Empty;
    public List<JsonElement> Elements { get; set; } = new List<JsonElement>();

    HashSet<JsonToken> JsonTags { get; } = new HashSet<JsonToken>() { JsonToken.StartObject, JsonToken.EndObject, JsonToken.StartArray, JsonToken.EndArray };
    HashSet<string> OdataItems { get; set; } = new HashSet<string>() { "/redfish/v1/" };

    /// <summary>
    /// 全てを読み込む
    /// </summary>
    /// <param name="path"></param>
    public void Read(string path)
    {
        // JSONファイルの存在確認
        var jsonfile = @$"{Root}\{path}";
        if (!System.IO.File.Exists(jsonfile)) return;

        // Redfish情報の読み込み
        using var file = File.OpenText(jsonfile);
        var reader = new JsonTextReader(file);
        while (reader.Read())
        {
            var val1 = reader.Value;
            Elements.Add(new JsonElement() { TokenType = reader.TokenType, Value = val1 });
 
            if (reader.TokenType != JsonToken.PropertyName) continue;   // プロパティ以外
            reader.Read();                                              // プロパティの値を読み込む
            var val2 = reader.Value;
            Elements.Add(new JsonElement() { TokenType = reader.TokenType, Value = val2 });

            if (val1 == null || val2 == null) continue;                 // 警告対策
            if (JsonTags.Contains(reader.TokenType)) continue;          // {} or [] 出現
            if (!((string)val1).Equals("@odata.id")) continue;          // @odata.id 以外
            if (OdataItems.Contains((string)val2)) continue;            // 同じ @odata.id が出現

            // @odata.id 処理
            OdataItems.Add((string)val2);                               // "@odata.id" 値を保持
            Elements.Add(new JsonElement() { TokenType = JsonToken.PropertyName, Value = "/* @odata.child */" });
            Read(((string)val2)[12..].Replace("/", "_") + ".json");     // 再帰
        }
    }

    /// <summary>
    /// マージしたJSONを出力する
    /// </summary>
    public void Write()
    {
        var sb = new StringBuilder();
        using var sw = new StringWriter(sb);
        using var writer = new JsonTextWriter(sw);
        writer.Formatting = Formatting.Indented;
        foreach (var elem in Elements)
        {
            switch (elem.TokenType)
            {
                case JsonToken.StartObject: writer.WriteStartObject(); break;
                case JsonToken.EndObject: writer.WriteEndObject(); break;
                case JsonToken.StartArray: writer.WriteStartArray(); break;
                case JsonToken.EndArray: writer.WriteEndArray(); break;
                case JsonToken.PropertyName: writer.WritePropertyName((string)(elem.Value ?? "")); break;
                default: writer.WriteValue(elem.Value); break;
            }
        }
        Console.WriteLine(sb.ToString());
    }
}

出力結果

今回用意した 3 つの入れ子構造の JSON を一つにマージさせることができました。 実際に試してみたい方は、以下の / @odata.child / の内容を切り出すと、 JSON ファイルが用意できると思います。

{
  "@odata.type": "#ServiceRoot.v1_14_0.ServiceRoot",
  "Id": "RootService",
  "Name": "Root Service",
  "RedfishVersion": "1.15.0",
  "UUID": "92384634-2938-2342-8820-489239905423",
  "ProtocolFeaturesSupported": {
    "ExpandQuery": {
      "ExpandAll": true,
      "Levels": true,
      "MaxLevels": 6,
      "Links": true,
      "NoLinks": true
    },
    "SelectQuery": false,
    "FilterQuery": false,
    "OnlyMemberQuery": true,
    "ExcerptQuery": true
  },
  "Systems": {
    "@odata.id": "/redfish/v1/Systems",
    "/* @odata.child */": {
      "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection",
      "Name": "Computer System Collection",
      "Members@odata.count": 1,
      "Members": [
        {
          "@odata.id": "/redfish/v1/Systems/437XR1138R2",
          "/* @odata.child */": {
            "@odata.type": "#ComputerSystem.v1_19_0.ComputerSystem",
            "Id": "437XR1138R2",
            "Name": "WebFrontEnd483",
            "SystemType": "Physical",
            "AssetTag": "Chicago-45Z-2381",
            "Manufacturer": "Contoso",
            "Model": "3500",
            "SubModel": "RX",
            "SKU": "8675309",
            "SerialNumber": "437XR1138R2",
            "PartNumber": "224071-J23",
            "Description": "Web Front End node",
            "UUID": "38947555-7742-3448-3784-823347823834",
            "HostName": "web483",
            "Status": {
              "State": "Enabled",
              "Health": "OK",
              "HealthRollup": "OK"
            },
            "HostingRoles": [
              "ApplicationServer"
            ],
            "IndicatorLED": "Off",
            "PowerState": "On",
            "Boot": {
              "BootSourceOverrideEnabled": "Once",
              "BootSourceOverrideTarget": "Pxe",
              "BootSourceOverrideTarget@Redfish.AllowableValues": [
                "None",
                "Pxe",
                "Cd",
                "Usb",
                "Hdd",
                "BiosSetup",
                "Utilities",
                "Diags",
                "SDCard",
                "UefiTarget"
              ],
              "BootSourceOverrideMode": "UEFI",
              "UefiTargetBootSourceOverride": "/0x31/0x33/0x01/0x01"
            },
            "TrustedModules": [
              {
                "FirmwareVersion": "1.13b",
                "InterfaceType": "TPM1_2",
                "Status": {
                  "State": "Enabled",
                  "Health": "OK"
                }
              }
            ],
            "Oem": {
              "Contoso": {
                "@odata.type": "#Contoso.ComputerSystem",
                "ProductionLocation": {
                  "FacilityName": "PacWest Production Facility",
                  "Country": "USA"
                }
              },
              "Chipwise": {
                "@odata.type": "#Chipwise.ComputerSystem",
                "Style": "Executive"
              }
            },
            "BootProgress": {
              "LastState": "OSRunning",
              "LastStateTime": "2021-03-13T07:14:13+09:00",
              "LastBootTimeSeconds": 676
            },
            "LastResetTime": "2021-03-13T07:02:57+09:00",
            "BiosVersion": "P79 v1.45 (12/06/2017)",
            "ProcessorSummary": {
              "Count": 2,
              "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series",
              "LogicalProcessorCount": 16,
              "CoreCount": 8,
              "Status": {
                "State": "Enabled",
                "Health": "OK",
                "HealthRollup": "OK"
              }
            },
            "MemorySummary": {
              "TotalSystemMemoryGiB": 96,
              "TotalSystemPersistentMemoryGiB": 0,
              "MemoryMirroring": "None",
              "Status": {
                "State": "Enabled",
                "Health": "OK",
                "HealthRollup": "OK"
              }
            },
            "Bios": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/Bios",
              "/* @odata.child */": null
            },
            "SecureBoot": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot",
              "/* @odata.child */": null
            },
            "Processors": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors",
              "/* @odata.child */": null
            },
            "Memory": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory",
              "/* @odata.child */": null
            },
            "EthernetInterfaces": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces",
              "/* @odata.child */": null
            },
            "SimpleStorage": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage",
              "/* @odata.child */": null
            },
            "LogServices": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices",
              "/* @odata.child */": null
            },
            "GraphicsControllers": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/GraphicsControllers",
              "/* @odata.child */": null
            },
            "USBControllers": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/USBControllers",
              "/* @odata.child */": null
            },
            "Certificates": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/Certificates",
              "/* @odata.child */": null
            },
            "VirtualMedia": {
              "@odata.id": "/redfish/v1/Systems/437XR1138R2/VirtualMedia",
              "/* @odata.child */": null
            },
            "Links": {
              "Chassis": [
                {
                  "@odata.id": "/redfish/v1/Chassis/1U",
                  "/* @odata.child */": null
                }
              ],
              "ManagedBy": [
                {
                  "@odata.id": "/redfish/v1/Managers/BMC",
                  "/* @odata.child */": null
                }
              ]
            },
            "Actions": {
              "#ComputerSystem.Reset": {
                "target": "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset",
                "ResetType@Redfish.AllowableValues": [
                  "On",
                  "ForceOff",
                  "GracefulShutdown",
                  "GracefulRestart",
                  "ForceRestart",
                  "Nmi",
                  "ForceOn",
                  "PushPowerButton"
                ]
              },
              "Oem": {
                "#Contoso.Reset": {
                  "target": "/redfish/v1/Systems/437XR1138R2/Oem/Contoso/Actions/Contoso.Reset"
                }
              }
            },
            "@odata.id": "/redfish/v1/Systems/437XR1138R2"
          }
        }
      ],
      "@odata.id": "/redfish/v1/Systems"
    }
  },
  "Chassis": {
    "@odata.id": "/redfish/v1/Chassis",
    "/* @odata.child */": null
  },
  "Managers": {
    "@odata.id": "/redfish/v1/Managers",
    "/* @odata.child */": null
  },
  "Tasks": {
    "@odata.id": "/redfish/v1/TaskService",
    "/* @odata.child */": null
  },
  "SessionService": {
    "@odata.id": "/redfish/v1/SessionService",
    "/* @odata.child */": null
  },
  "AccountService": {
    "@odata.id": "/redfish/v1/AccountService",
    "/* @odata.child */": null
  },
  "EventService": {
    "@odata.id": "/redfish/v1/EventService",
    "/* @odata.child */": null
  },
  "Registries": {
    "@odata.id": "/redfish/v1/Registries",
    "/* @odata.child */": null
  },
  "UpdateService": {
    "@odata.id": "/redfish/v1/UpdateService",
    "/* @odata.child */": null
  },
  "CertificateService": {
    "@odata.id": "/redfish/v1/CertificateService",
    "/* @odata.child */": null
  },
  "KeyService": {
    "@odata.id": "/redfish/v1/KeyService",
    "/* @odata.child */": null
  },
  "Links": {
    "Sessions": {
      "@odata.id": "/redfish/v1/SessionService/Sessions",
      "/* @odata.child */": null
    }
  },
  "ComponentIntegrity": {
    "@odata.id": "/redfish/v1/ComponentIntegrity",
    "/* @odata.child */": null
  },
  "Oem": {},
  "@odata.id": "/redfish/v1/"
}

まとめ

今回は、Redfish の JSON を例に入れ子構造の JSON 群をマージする方法を紹介しました。 JSON ファイルを読み込んでいる箇所を REST API で @odata.id 情報を取得するようにすると、 Redfish の情報を一括で取得できるようになると思います。 今回は、Redfish はサーバを用意する必要があるため、 JSON ファイルとして簡単に試せる形式にしてみました。

では、皆さん、よい旅を。

後日談

実際にサーバから Redfish で情報を取得するように修正しました。 Web アクセス版はこちら。

sabakunotabito.hatenablog.com