Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ Notes:
- Volume: set `tieringPolicy` on the volume (for example `tierAction: ENABLED`, optional `coolingThresholdDays`, optional `hotTierBypassModeEnabled`).
- Hybrid replication: set `hybridReplicationParameters` on the volume (for example `replicationSchedule: HOURLY` and `hybridReplicationType: CONTINUOUS_REPLICATION`) along with peer details (cluster/SVM/IPs).
- Large capacity volumes: set `largeCapacity: true` (Premium/Extreme only; minimum 15 TiB) and optionally `multipleEndpoints: true` for multiple storage endpoints. See the volume limits and overview docs.
- SMB attributes (only when `protocols` includes `SMB`):
- Boolean shortcuts on `gcnv_volume_create`:
- `smbEncryptData: true` → SMB encryption (`ENCRYPT_DATA`)
- `smbHideShare: true` → hidden / non-browsable share (`NON_BROWSABLE`)
- `smbAccessBasedEnumeration: true` → access-based enumeration (`ACCESS_BASED_ENUMERATION`) — controls the visibility of files and folders based on the permissions assigned to the user
- `smbContinuouslyAvailable: true` → CA share for SQL Server / FSLogix (`CONTINUOUSLY_AVAILABLE`); **permanent** on the volume.
- Advanced: `smbSettings: ["OPLOCKS", ...]` accepts raw API enum names and is merged with the booleans above. Do not pass `SMB_SETTINGS_UNSPECIFIED`. `BROWSABLE` together with `NON_BROWSABLE` (or `BROWSABLE` with `smbHideShare`) are rejected. `NON_BROWSABLE` and `CONTINUOUSLY_AVAILABLE` together (or `smbHideShare` with `smbContinuouslyAvailable`) are invalid — CA shares must be browsable.
- Any SMB flag without `protocols: ["SMB", ...]` is rejected.
- `CONTINUOUSLY_AVAILABLE` is **not supported on FLEX storage pools** — the server rejects the request before calling the API. Tell users to use STANDARD / PREMIUM / EXTREME for CA shares.
Comment thread
jiljithcp marked this conversation as resolved.
- Confirm CA explicitly with the user before creating, since it cannot be turned off later.

### Snapshots

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ HTTP endpoint: `http://localhost:<port>/message`

**Large capacity volumes:** Set `largeCapacity: true` (Premium/Extreme only, minimum 15 TiB). Optional `multipleEndpoints: true`.

**SMB attributes:** When `protocols` includes `SMB`, `gcnv_volume_create` accepts optional SMB feature flags that map to the `smbSettings` field on the volume:

- `smbEncryptData: true` → `ENCRYPT_DATA` (require SMB encryption in flight)
- `smbHideShare: true` → `NON_BROWSABLE` (hide the share from browse lists)
- `smbAccessBasedEnumeration: true` → `ACCESS_BASED_ENUMERATION` (ABE) — controls the visibility of files and folders based on the permissions assigned to the user
- `smbContinuouslyAvailable: true` → `CONTINUOUSLY_AVAILABLE` (CA share for SQL Server / FSLogix; choice is permanent on the volume)
- `smbSettings: ["OPLOCKS", ...]` — additional API enum values; merged with the booleans above. Do not pass `SMB_SETTINGS_UNSPECIFIED`; `BROWSABLE` and `NON_BROWSABLE` (or `BROWSABLE` together with `smbHideShare`) are rejected. `NON_BROWSABLE` and `CONTINUOUSLY_AVAILABLE` together (or `smbHideShare` with `smbContinuouslyAvailable`) are also rejected — CA shares must be browsable.

These flags require `protocols` to include `SMB`. `CONTINUOUSLY_AVAILABLE` is **not supported on `FLEX` storage pools** — the request is rejected before reaching the API. Use `STANDARD`, `PREMIUM`, or `EXTREME` for CA shares.
Comment thread
jiljithcp marked this conversation as resolved.

### Snapshot Tools

| Tool | Description |
Expand Down
259 changes: 259 additions & 0 deletions src/tools/handlers/volume-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,265 @@ describe('volume-handler', () => {
});
});

it('createVolumeHandler passes smbSettings when SMB flags are set', async () => {
const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-smb' }]);
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'volsmb2',
capacityGib: 10,
protocols: ['SMB'],
smbEncryptData: true,
smbHideShare: true,
smbAccessBasedEnumeration: true,
});

expect(createVolume.mock.calls[0]?.[0]?.volume?.storagePool).toBe('sp1');
Comment thread
jiljithcp marked this conversation as resolved.
expect(createVolume.mock.calls[0]?.[0]?.volume?.smbSettings).toEqual(
expect.arrayContaining(['ENCRYPT_DATA', 'NON_BROWSABLE', 'ACCESS_BASED_ENUMERATION'])
);
expect(createVolume.mock.calls[0]?.[0]?.volume?.smbSettings).toHaveLength(3);
});

it('createVolumeHandler merges smbSettings array with boolean flags', async () => {
const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-smb2' }]);
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['OPLOCKS'],
smbEncryptData: true,
});

const smb = createVolume.mock.calls[0]?.[0]?.volume?.smbSettings as string[];
expect(smb).toEqual(expect.arrayContaining(['OPLOCKS', 'ENCRYPT_DATA']));
expect(smb).toHaveLength(2);
});

it('createVolumeHandler rejects smb flags when protocols omit SMB', async () => {
const createVolume = vi.fn();
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
const result = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['NFSV3'],
smbEncryptData: true,
});

expect(createVolume).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect((result.content?.[0] as any)?.text).toMatch(/SMB/);
});

it('createVolumeHandler rejects invalid smbSettings entries', async () => {
const createVolume = vi.fn();
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
const result = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['NOT_A_REAL_SETTING'],
});

expect(createVolume).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
});

it('createVolumeHandler rejects smbSettings containing only SMB_SETTINGS_UNSPECIFIED', async () => {
const createVolume = vi.fn();
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
const result = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['SMB_SETTINGS_UNSPECIFIED'],
});

expect(createVolume).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect((result.content?.[0] as any)?.text).toMatch(/SMB_SETTINGS_UNSPECIFIED/);
});

it('createVolumeHandler rejects conflicting BROWSABLE and NON_BROWSABLE', async () => {
const createVolume = vi.fn();
createClientMock.mockReturnValue({ createVolume });

const { createVolumeHandler } = await import('./volume-handler.js');
const both = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['BROWSABLE', 'NON_BROWSABLE'],
});
expect(both.isError).toBe(true);
expect(createVolume).not.toHaveBeenCalled();

const hideWithBrowse = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v2',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['BROWSABLE'],
smbHideShare: true,
});
expect(hideWithBrowse.isError).toBe(true);
expect(createVolume).not.toHaveBeenCalled();
});

it('createVolumeHandler rejects NON_BROWSABLE + CONTINUOUSLY_AVAILABLE', async () => {
const createVolume = vi.fn();
const getStoragePool = vi.fn();
createClientMock.mockReturnValue({ createVolume, getStoragePool });

const { createVolumeHandler } = await import('./volume-handler.js');

// Both as enum values in smbSettings.
const bothEnum = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['NON_BROWSABLE', 'CONTINUOUSLY_AVAILABLE'],
});
expect(bothEnum.isError).toBe(true);
expect((bothEnum.content?.[0] as any)?.text).toMatch(/NON_BROWSABLE/);
expect((bothEnum.content?.[0] as any)?.text).toMatch(/CONTINUOUSLY_AVAILABLE/);
expect(getStoragePool).not.toHaveBeenCalled();
expect(createVolume).not.toHaveBeenCalled();

// Both as boolean flags.
const bothFlags = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v2',
capacityGib: 10,
protocols: ['SMB'],
smbHideShare: true,
smbContinuouslyAvailable: true,
});
expect(bothFlags.isError).toBe(true);
expect(getStoragePool).not.toHaveBeenCalled();
expect(createVolume).not.toHaveBeenCalled();

// Mixed: flag + enum.
const mixed = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'sp1',
volumeId: 'v3',
capacityGib: 10,
protocols: ['SMB'],
smbHideShare: true,
smbSettings: ['CONTINUOUSLY_AVAILABLE'],
});
expect(mixed.isError).toBe(true);
expect(getStoragePool).not.toHaveBeenCalled();
expect(createVolume).not.toHaveBeenCalled();
});

it('createVolumeHandler rejects CONTINUOUSLY_AVAILABLE on FLEX pools (boolean flag)', async () => {
const createVolume = vi.fn();
const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'FLEX' }]);
createClientMock.mockReturnValue({ createVolume, getStoragePool });

const { createVolumeHandler } = await import('./volume-handler.js');
const result = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'pool-flex',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbContinuouslyAvailable: true,
});

expect(getStoragePool).toHaveBeenCalledWith({
name: 'projects/p1/locations/us-central1/storagePools/pool-flex',
});
expect(createVolume).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect((result.content?.[0] as any)?.text).toMatch(/FLEX/);
});

it('createVolumeHandler rejects CONTINUOUSLY_AVAILABLE on FLEX pools (smbSettings array)', async () => {
const createVolume = vi.fn();
const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'flex' }]);
createClientMock.mockReturnValue({ createVolume, getStoragePool });

const { createVolumeHandler } = await import('./volume-handler.js');
const result = await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'projects/p1/locations/us-central1/storagePools/p1',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbSettings: ['CONTINUOUSLY_AVAILABLE'],
});

expect(getStoragePool).toHaveBeenCalledWith({
name: 'projects/p1/locations/us-central1/storagePools/p1',
});
expect(createVolume).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
});

it('createVolumeHandler passes CONTINUOUSLY_AVAILABLE when pool is not FLEX', async () => {
const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-ca' }]);
const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'STANDARD' }]);
createClientMock.mockReturnValue({ createVolume, getStoragePool });

const { createVolumeHandler } = await import('./volume-handler.js');
await createVolumeHandler({
projectId: 'p1',
location: 'us-central1',
storagePoolId: 'pool-std',
volumeId: 'v1',
capacityGib: 10,
protocols: ['SMB'],
smbContinuouslyAvailable: true,
});

expect(getStoragePool).toHaveBeenCalled();
expect(createVolume.mock.calls[0]?.[0]?.volume?.storagePool).toBe('pool-std');
Comment thread
jiljithcp marked this conversation as resolved.
expect(createVolume.mock.calls[0]?.[0]?.volume?.smbSettings).toContain(
'CONTINUOUSLY_AVAILABLE'
);
});

it('createVolumeHandler supports Large Capacity Volumes (Premium/Extreme only)', async () => {
const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]);
const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'PREMIUM' }]);
Expand Down
Loading
Loading