diff options
author | tarum <tarum@yandex-team.com> | 2023-08-23 11:24:46 +0300 |
---|---|---|
committer | tarum <tarum@yandex-team.com> | 2023-08-23 11:47:47 +0300 |
commit | e0df831cdf6a7edf36376abeb1304f1df919f94d (patch) | |
tree | c68c21d97da0e27f6c81aba7bf21e90295c06377 | |
parent | 0230f1914a9d71ee56e07f074d4276f4bad093ad (diff) | |
download | ydb-e0df831cdf6a7edf36376abeb1304f1df919f94d.tar.gz |
KIKIMR-17274: Support read-only VDisk in dstool and rename command for read-only internally
-rw-r--r-- | ydb/apps/dstool/lib/arg_parser.py | 14 | ||||
-rw-r--r-- | ydb/apps/dstool/lib/commands.py | 7 | ||||
-rw-r--r-- | ydb/apps/dstool/lib/dstool_cmd_vdisk_list.py | 2 | ||||
-rw-r--r-- | ydb/apps/dstool/lib/dstool_cmd_vdisk_set_read_only.py | 64 | ||||
-rw-r--r-- | ydb/apps/dstool/lib/ya.make | 1 | ||||
-rw-r--r-- | ydb/core/blobstorage/ut_blobstorage/lib/env.h | 30 | ||||
-rw-r--r-- | ydb/core/blobstorage/ut_blobstorage/read_only_vdisk.cpp | 15 | ||||
-rw-r--r-- | ydb/core/mind/bscontroller/bsc.cpp | 3 | ||||
-rw-r--r-- | ydb/core/mind/bscontroller/cmds_storage_pool.cpp | 38 | ||||
-rw-r--r-- | ydb/core/mind/bscontroller/config.cpp | 1 | ||||
-rw-r--r-- | ydb/core/mind/bscontroller/config.h | 3 | ||||
-rw-r--r-- | ydb/core/mind/bscontroller/config_cmd.cpp | 3 | ||||
-rw-r--r-- | ydb/core/protos/blobstorage_config.proto | 13 |
13 files changed, 112 insertions, 82 deletions
diff --git a/ydb/apps/dstool/lib/arg_parser.py b/ydb/apps/dstool/lib/arg_parser.py index 50aeea02bb5..e924feb98fd 100644 --- a/ydb/apps/dstool/lib/arg_parser.py +++ b/ydb/apps/dstool/lib/arg_parser.py @@ -543,8 +543,18 @@ class ArgumentParser: max_args = free_args_count if not self._subparsers else 'unlimited' print() print_with_word_wrapping('Free args: min: {0} max: {1}'.format(min_args, max_args)) - # ! TODO(FREEARGS) + + if self._free_arguments: + for free_argument in self._free_arguments: + opt, help = free_argument.make_help_lines() + optional_empty_line = '' if len(opt) < max_length else '\n' + print_with_word_wrapping( + f'{optional_empty_line}{help}', + initial_indent=f' {opt:<{max_length-2}}', + subsequent_indent=f'{"":<{max_length}}') if self._subparsers: + if self._free_arguments: + print() line = ' <subcommand> {0}'.format(', '.join((x.metainfo.name for x in self._subparsers._subparsers))) print_with_word_wrapping(line, subsequent_indent=' ') @@ -755,7 +765,7 @@ class Parser: if self._free_arg_idx >= self._free_args_limit: print_error_with_usage(self, f'Unexpected free arg {value}') if self._free_arg_idx < len(self.parser._free_arguments): - self.parser._free_arguments[self._free_arg_idx].apply_value(value) + self.parser._free_arguments[self._free_arg_idx].apply(value) self._shift() else: self.parser._subparsers._value.apply_value(value) diff --git a/ydb/apps/dstool/lib/commands.py b/ydb/apps/dstool/lib/commands.py index d203b15be32..e2bb5365d38 100644 --- a/ydb/apps/dstool/lib/commands.py +++ b/ydb/apps/dstool/lib/commands.py @@ -5,9 +5,10 @@ import ydb.apps.dstool.lib.dstool_cmd_pdisk_remove_by_serial as pdisk_remove_by_ import ydb.apps.dstool.lib.dstool_cmd_pdisk_set as pdisk_set import ydb.apps.dstool.lib.dstool_cmd_pdisk_list as pdisk_list -import ydb.apps.dstool.lib.dstool_cmd_vdisk_remove_donor as vdisk_remove_donor import ydb.apps.dstool.lib.dstool_cmd_vdisk_evict as vdisk_evict import ydb.apps.dstool.lib.dstool_cmd_vdisk_list as vdisk_list +import ydb.apps.dstool.lib.dstool_cmd_vdisk_set_read_only as vdisk_set_read_only +import ydb.apps.dstool.lib.dstool_cmd_vdisk_remove_donor as vdisk_remove_donor import ydb.apps.dstool.lib.dstool_cmd_vdisk_wipe as vdisk_wipe import ydb.apps.dstool.lib.dstool_cmd_group_add as group_add @@ -48,14 +49,14 @@ modules = [ group_check, group_decommit, group_show_blob_info, group_show_storage_efficiency, group_show_usage_by_tablets, group_state, group_take_snapshot, group_add, group_list, group_virtual_create, group_virtual_cancel, pdisk_add_by_serial, pdisk_remove_by_serial, pdisk_set, pdisk_list, - vdisk_remove_donor, vdisk_evict, vdisk_list, vdisk_wipe, + vdisk_evict, vdisk_list, vdisk_set_read_only, vdisk_remove_donor, vdisk_wipe, device_list, ] default_structure = [ ('device', ['list']), ('pdisk', ['add-by-serial', 'remove-by-serial', 'set', 'list']), - ('vdisk', ['evict', 'remove-donor', 'wipe', 'list']), + ('vdisk', ['evict', 'list', 'set-read-only', 'remove-donor', 'wipe']), ('group', ['add', 'check', 'decommit', ('show', ['blob-info', 'storage-efficiency', 'usage-by-tablets']), 'state', 'take-snapshot', 'list', ('virtual', ['create', 'cancel'])]), ('pool', ['list', ('create', ['virtual'])]), ('box', ['list']), diff --git a/ydb/apps/dstool/lib/dstool_cmd_vdisk_list.py b/ydb/apps/dstool/lib/dstool_cmd_vdisk_list.py index 87db50b5a50..a88c6b4333c 100644 --- a/ydb/apps/dstool/lib/dstool_cmd_vdisk_list.py +++ b/ydb/apps/dstool/lib/dstool_cmd_vdisk_list.py @@ -66,6 +66,7 @@ def do(args): 'VSlotId', 'VSlotStatus', 'IsDonor', + 'ReadOnly', ] col_units = { 'Usage': '%', @@ -103,6 +104,7 @@ def do(args): row['VSlotId'] = vslot_data.VSlotId row['VSlotStatus'] = vslot.Status row['IsDonor'] = group_map[group].GroupGeneration != vslot.GroupGeneration + row['ReadOnly'] = vslot.ReadOnly row['FailRealmIdx'] = vslot.FailRealmIdx row['FailDomainIdx'] = vslot.FailDomainIdx row['VDiskIdx'] = vslot.VDiskIdx diff --git a/ydb/apps/dstool/lib/dstool_cmd_vdisk_set_read_only.py b/ydb/apps/dstool/lib/dstool_cmd_vdisk_set_read_only.py new file mode 100644 index 00000000000..479df5b05db --- /dev/null +++ b/ydb/apps/dstool/lib/dstool_cmd_vdisk_set_read_only.py @@ -0,0 +1,64 @@ +import ydb.apps.dstool.lib.common as common +import sys + +description = 'Set vdisk read-only mode (experimental mode that might help restore data in certain situations)' + + +def add_options(p): + p.add_argument('--vdisk-id', type=str, required=True, help='Vdisk id in format [GroupId:_:FailRealm:FailDomain:VDiskIdx]') + p.add_argument('value', type=str, choices=('true', 'false'), help='Use one of the values to set or unset read-only mode') + common.add_ignore_degraded_group_check_option(p) + common.add_ignore_disintegrated_group_check_option(p) + common.add_ignore_failure_model_group_check_option(p) + common.add_basic_format_options(p) + + +def create_request(args, vslot): + assert not args.dry_run, '--dry-run is not supported for this command' + request = common.create_bsc_request(args) + cmd = request.Command.add().SetVDiskReadOnly + if args.value == "true": + cmd.Value = True + elif args.value == "false": + cmd.Value = False + else: + raise Exception("invalid 'value' argument: {}".format(args.value)) + cmd.VSlotId.NodeId = vslot.VSlotId.NodeId + cmd.VSlotId.PDiskId = vslot.VSlotId.PDiskId + cmd.VSlotId.VSlotId = vslot.VSlotId.VSlotId + cmd.VDiskId.GroupID = vslot.GroupId + cmd.VDiskId.GroupGeneration = vslot.GroupGeneration + cmd.VDiskId.Ring = vslot.FailRealmIdx + cmd.VDiskId.Domain = vslot.FailDomainIdx + cmd.VDiskId.VDisk = vslot.VDiskIdx + return request + + +def perform_request(request): + return common.invoke_bsc_request(request) + + +def is_successful_response(response): + return common.is_successful_bsc_response(response) + + +def do(args): + base_config = common.fetch_base_config() + vslots = common.get_vslots_by_vdisk_ids(base_config, [args.vdisk_id]) + if len(vslots) != 1: + common.print_status(args, success=False, error_reason='Unexpected number of found vslots: {}'.format(len(vslots))) + sys.exit(1) + + success = True + error_reason = '' + vslot = vslots[0] + + request = create_request(args, vslot) + response = perform_request(request) + if not is_successful_response(response): + success = False + error_reason += 'Request has failed: \n{0}\n{1}\n'.format(request, response) + + common.print_status(args, success, error_reason) + if not success: + sys.exit(1) diff --git a/ydb/apps/dstool/lib/ya.make b/ydb/apps/dstool/lib/ya.make index 9016b190c27..a6c3478452a 100644 --- a/ydb/apps/dstool/lib/ya.make +++ b/ydb/apps/dstool/lib/ya.make @@ -18,6 +18,7 @@ PY_SRCS( dstool_cmd_vdisk_evict.py dstool_cmd_vdisk_list.py + dstool_cmd_vdisk_set_read_only.py dstool_cmd_vdisk_remove_donor.py dstool_cmd_vdisk_wipe.py diff --git a/ydb/core/blobstorage/ut_blobstorage/lib/env.h b/ydb/core/blobstorage/ut_blobstorage/lib/env.h index 3afd44ff8ce..9634cd44bb8 100644 --- a/ydb/core/blobstorage/ut_blobstorage/lib/env.h +++ b/ydb/core/blobstorage/ut_blobstorage/lib/env.h @@ -5,7 +5,6 @@ #include "node_warden_mock.h" #include <ydb/core/driver_lib/version/version.h> -#include <ydb/core/mind/bscontroller/mood.h> #include <library/cpp/testing/unittest/registar.h> @@ -687,37 +686,16 @@ struct TEnvironmentSetup { vslot->SetVSlotId(vslotId); } - void PutVDiskToNormal(ui32 nodeId, ui32 pdiskId, ui32 vslotId, const TVDiskID& vdiskId) { + void SetVDiskReadOnly(ui32 nodeId, ui32 pdiskId, ui32 vslotId, const TVDiskID& vdiskId, bool value) { NKikimrBlobStorage::TConfigRequest request; - auto *roCmd = request.AddCommand()->MutablePutVDiskToNormal(); + auto *roCmd = request.AddCommand()->MutableSetVDiskReadOnly(); FillVSlotId(nodeId, pdiskId, vslotId, roCmd->MutableVSlotId()); VDiskIDFromVDiskID(vdiskId, roCmd->MutableVDiskId()); - Cerr << "Invoking PutVDiskToNormal for vdisk " << vdiskId.ToString() << Endl; + roCmd->SetValue(value); + Cerr << "Invoking SetVDiskReadOnly for vdisk " << vdiskId.ToString() << Endl; auto response = Invoke(request); UNIT_ASSERT_C(response.GetSuccess(), response.GetErrorDescription()); - } - void PutVDiskToReadOnly(ui32 nodeId, ui32 pdiskId, ui32 vslotId, const TVDiskID& vdiskId) { - NKikimrBlobStorage::TConfigRequest request; - auto *roCmd = request.AddCommand()->MutablePutVDiskToReadOnly(); - FillVSlotId(nodeId, pdiskId, vslotId, roCmd->MutableVSlotId()); - VDiskIDFromVDiskID(vdiskId, roCmd->MutableVDiskId()); - Cerr << "Invoking PutVDiskToReadOnly for vdisk " << vdiskId.ToString() << Endl; - auto response = Invoke(request); - UNIT_ASSERT_C(response.GetSuccess(), response.GetErrorDescription()); - } - - void PutVDiskToMood(ui32 nodeId, ui32 pdiskId, ui32 vslotId, const TVDiskID& vdiskId, NKikimr::NBsController::TMood::EValue mood) { - switch (mood) { - case NKikimr::NBsController::TMood::Normal: - PutVDiskToNormal(nodeId, pdiskId, vslotId, vdiskId); - break; - case NKikimr::NBsController::TMood::ReadOnly: - PutVDiskToReadOnly(nodeId, pdiskId, vslotId, vdiskId); - break; - default: - ythrow yexception() << "unsupported mood: " << NKikimr::NBsController::TMood::Name(mood); - } } void UpdateDriveStatus(ui32 nodeId, ui32 pdiskId, NKikimrBlobStorage::EDriveStatus status, diff --git a/ydb/core/blobstorage/ut_blobstorage/read_only_vdisk.cpp b/ydb/core/blobstorage/ut_blobstorage/read_only_vdisk.cpp index 9417f6fea5e..7edf11143d3 100644 --- a/ydb/core/blobstorage/ut_blobstorage/read_only_vdisk.cpp +++ b/ydb/core/blobstorage/ut_blobstorage/read_only_vdisk.cpp @@ -81,20 +81,19 @@ Y_UNIT_TEST_SUITE(ReadOnlyVDisk) { ++step; readAllBlobs(step); - using NKikimr::NBsController::TMood; - auto putVDiskToMood = [&] (ui32 position, TMood::EValue mood) { + auto setVDiskReadOnly = [&] (ui32 position, bool value) { const TVDiskID& someVDisk = info->GetVDiskId(position); auto baseConfig = env.FetchBaseConfig(); const auto& somePDisk = baseConfig.GetPDisk(position); const auto& someVSlot = baseConfig.GetVSlot(position); - Cerr << "Putting VDisk to mood " << TMood::Name(mood) << " for position " << position << Endl; - env.PutVDiskToMood(somePDisk.GetNodeId(), somePDisk.GetPDiskId(), someVSlot.GetVSlotId().GetVSlotId(), someVDisk, mood); + Cerr << "Setting VDisk read-only to " << value << " for position " << position << Endl; + env.SetVDiskReadOnly(somePDisk.GetNodeId(), somePDisk.GetPDiskId(), someVSlot.GetVSlotId().GetVSlotId(), someVDisk, value); env.Sim(TDuration::Seconds(30)); }; Cerr << "=== Putting VDisk #0 to read-only ===" << Endl; - putVDiskToMood(0, TMood::ReadOnly); + setVDiskReadOnly(0, true); Cerr << "=== Write 10 blobs, expect some VDisks refuse parts but writes go through ===" << Endl; for (ui32 i = 0; i < 10; ++i) { @@ -105,8 +104,8 @@ Y_UNIT_TEST_SUITE(ReadOnlyVDisk) { readAllBlobs(step); Cerr << "=== Put 2 more VDisks to read-only ===" << Endl; - putVDiskToMood(1, TMood::ReadOnly); - putVDiskToMood(2, TMood::ReadOnly); + setVDiskReadOnly(1, true); + setVDiskReadOnly(2, true); Cerr << "=== Write 10 more blobs, expect errors ===" << Endl; for (ui32 i = 0; i < 10; ++i) { @@ -117,7 +116,7 @@ Y_UNIT_TEST_SUITE(ReadOnlyVDisk) { readAllBlobs(step); Cerr << "=== Restoring to normal VDisk #0 ===" << Endl; - putVDiskToMood(0, TMood::Normal); + setVDiskReadOnly(0, false); Cerr << "=== Write 10 blobs, expect some VDisks refuse parts but the writes still go through ===" << Endl; for (ui32 i = 0; i < 10; ++i) { diff --git a/ydb/core/mind/bscontroller/bsc.cpp b/ydb/core/mind/bscontroller/bsc.cpp index 61c0a674472..36dc3684974 100644 --- a/ydb/core/mind/bscontroller/bsc.cpp +++ b/ydb/core/mind/bscontroller/bsc.cpp @@ -329,8 +329,7 @@ ui32 TBlobStorageController::GetEventPriority(IEventHandle *ev) { case NKikimrBlobStorage::TConfigRequest::TCommand::kWipeVDisk: case NKikimrBlobStorage::TConfigRequest::TCommand::kSanitizeGroup: case NKikimrBlobStorage::TConfigRequest::TCommand::kCancelVirtualGroup: - case NKikimrBlobStorage::TConfigRequest::TCommand::kPutVDiskToNormal: - case NKikimrBlobStorage::TConfigRequest::TCommand::kPutVDiskToReadOnly: + case NKikimrBlobStorage::TConfigRequest::TCommand::kSetVDiskReadOnly: return 2; // read-write commands go with higher priority as they are needed to keep cluster intact case NKikimrBlobStorage::TConfigRequest::TCommand::kReadHostConfig: diff --git a/ydb/core/mind/bscontroller/cmds_storage_pool.cpp b/ydb/core/mind/bscontroller/cmds_storage_pool.cpp index 80c9b3b5e18..fde9fddaf58 100644 --- a/ydb/core/mind/bscontroller/cmds_storage_pool.cpp +++ b/ydb/core/mind/bscontroller/cmds_storage_pool.cpp @@ -676,7 +676,7 @@ namespace NKikimr::NBsController { } } - void TBlobStorageController::TConfigState::ExecuteStep(const NKikimrBlobStorage::TPutVDiskToNormal& cmd, TStatus& /*status*/) { + void TBlobStorageController::TConfigState::ExecuteStep(const NKikimrBlobStorage::TSetVDiskReadOnly& cmd, TStatus& /*status*/) { // first, find matching vslot const TVSlotId& vslotId = cmd.GetVSlotId(); TVSlotInfo *vslot = VSlots.FindForUpdate(vslotId); @@ -690,39 +690,21 @@ namespace NKikimr::NBsController { throw TExVDiskIdIncorrect(vdiskId, vslotId); } + // then validate transition direction + const TMood::EValue currentMood = static_cast<TMood::EValue>(vslot->Mood); + const TMood::EValue targetMood = cmd.GetValue() ? TMood::ReadOnly : TMood::Normal; bool allowedTransition = ( - vslot->Mood == TMood::Normal || - vslot->Mood == TMood::ReadOnly + (currentMood == TMood::Normal && targetMood == TMood::ReadOnly) || + (currentMood == TMood::ReadOnly && targetMood == TMood::Normal) ); - if (!allowedTransition) { - throw TExError() << "unable to transition VDisk to normal from " << TMood::Name(static_cast<TMood::EValue>(vslot->Mood)); - } - - TGroupInfo *group = Groups.FindForUpdate(vslot->GroupId); - vslot->Mood = TMood::Normal; - vslot->Status = NKikimrBlobStorage::EVDiskStatus::INIT_PENDING; - vslot->IsReady = false; - GroupFailureModelChanged.insert(group->ID); - group->CalculateGroupStatus(); - } - - void TBlobStorageController::TConfigState::ExecuteStep(const NKikimrBlobStorage::TPutVDiskToReadOnly& cmd, TStatus& /*status*/) { - // first, find matching vslot - const TVSlotId& vslotId = cmd.GetVSlotId(); - TVSlotInfo *vslot = VSlots.FindForUpdate(vslotId); - if (!vslot) { - throw TExVSlotNotFound(vslotId); - } - - // second, validate vdisk id - const TVDiskID& vdiskId = VDiskIDFromVDiskID(cmd.GetVDiskId()); - if (vslot->GetVDiskId() != vdiskId) { - throw TExVDiskIdIncorrect(vdiskId, vslotId); + throw TExError() << "unable to transition VDisk" << + " from " << TMood::Name(currentMood) << + " to " << TMood::Name(targetMood); } TGroupInfo *group = Groups.FindForUpdate(vslot->GroupId); - vslot->Mood = TMood::ReadOnly; + vslot->Mood = targetMood; vslot->Status = NKikimrBlobStorage::EVDiskStatus::INIT_PENDING; vslot->IsReady = false; GroupFailureModelChanged.insert(group->ID); diff --git a/ydb/core/mind/bscontroller/config.cpp b/ydb/core/mind/bscontroller/config.cpp index 6500f6ed70c..92fb6f78e8a 100644 --- a/ydb/core/mind/bscontroller/config.cpp +++ b/ydb/core/mind/bscontroller/config.cpp @@ -927,6 +927,7 @@ namespace NKikimr::NBsController { }); } pb->SetReady(vslot.IsReady); + pb->SetReadOnly(vslot.Mood == TMood::ReadOnly); } void TBlobStorageController::Serialize(NKikimrBlobStorage::TBaseConfig::TGroup *pb, const TGroupInfo &group) { diff --git a/ydb/core/mind/bscontroller/config.h b/ydb/core/mind/bscontroller/config.h index 936bd471201..aa3d398e593 100644 --- a/ydb/core/mind/bscontroller/config.h +++ b/ydb/core/mind/bscontroller/config.h @@ -286,8 +286,7 @@ namespace NKikimr { void ExecuteStep(const NKikimrBlobStorage::TWipeVDisk& cmd, TStatus& status); void ExecuteStep(const NKikimrBlobStorage::TSanitizeGroup& cmd, TStatus& status); void ExecuteStep(const NKikimrBlobStorage::TCancelVirtualGroup& cmd, TStatus& status); - void ExecuteStep(const NKikimrBlobStorage::TPutVDiskToNormal& cmd, TStatus& status); - void ExecuteStep(const NKikimrBlobStorage::TPutVDiskToReadOnly& cmd, TStatus& status); + void ExecuteStep(const NKikimrBlobStorage::TSetVDiskReadOnly& cmd, TStatus& status); }; } // NBsController diff --git a/ydb/core/mind/bscontroller/config_cmd.cpp b/ydb/core/mind/bscontroller/config_cmd.cpp index 44850266611..7c2437a6106 100644 --- a/ydb/core/mind/bscontroller/config_cmd.cpp +++ b/ydb/core/mind/bscontroller/config_cmd.cpp @@ -330,8 +330,7 @@ namespace NKikimr::NBsController { HANDLE_COMMAND(WipeVDisk) HANDLE_COMMAND(SanitizeGroup) HANDLE_COMMAND(CancelVirtualGroup) - HANDLE_COMMAND(PutVDiskToNormal) - HANDLE_COMMAND(PutVDiskToReadOnly) + HANDLE_COMMAND(SetVDiskReadOnly) case NKikimrBlobStorage::TConfigRequest::TCommand::kAddMigrationPlan: case NKikimrBlobStorage::TConfigRequest::TCommand::kDeleteMigrationPlan: diff --git a/ydb/core/protos/blobstorage_config.proto b/ydb/core/protos/blobstorage_config.proto index 37ef5dc9412..e2755ad56f9 100644 --- a/ydb/core/protos/blobstorage_config.proto +++ b/ydb/core/protos/blobstorage_config.proto @@ -480,15 +480,10 @@ message TCancelVirtualGroup { uint32 GroupId = 1; // id of a group we are going to cancel } -// An attempt to transition to the normal mode, e.g. from the read-only mode. -message TPutVDiskToNormal { - NKikimrBlobStorage.TVSlotId VSlotId = 1; - NKikimrBlobStorage.TVDiskID VDiskId = 2; -} - -message TPutVDiskToReadOnly { +message TSetVDiskReadOnly { NKikimrBlobStorage.TVSlotId VSlotId = 1; NKikimrBlobStorage.TVDiskID VDiskId = 2; + bool Value = 3; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -533,8 +528,7 @@ message TConfigRequest { TSanitizeGroup SanitizeGroup = 42; TReadSettings ReadSettings = 43; TCancelVirtualGroup CancelVirtualGroup = 44; - TPutVDiskToReadOnly PutVDiskToReadOnly = 45; - TPutVDiskToNormal PutVDiskToNormal = 46; + TSetVDiskReadOnly SetVDiskReadOnly = 47; // commands intended for internal use TReassignGroupDisk ReassignGroupDisk = 19; @@ -632,6 +626,7 @@ message TBaseConfig { string Status = 10; // textual representation of EVDiskStatus or empty string if status is not known/reported repeated TDonorDisk Donors = 11; bool Ready = 12; // is disk READY in terms of BSC (stable READY status for some period of time) + bool ReadOnly = 13; } message TVirtualGroupInfo { NKikimrBlobStorage.EVirtualGroupState State = 1; |