@gcnv/gcnv-mcp-server 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GEMINI.md +200 -0
- package/LICENSE +201 -0
- package/README.md +374 -0
- package/build/index.js +185 -0
- package/build/logger.js +19 -0
- package/build/registry/register-tools.js +101 -0
- package/build/registry/tool-registry.js +27 -0
- package/build/tools/active-directory-tools.js +124 -0
- package/build/tools/backup-policy-tools.js +140 -0
- package/build/tools/backup-tools.js +178 -0
- package/build/tools/backup-vault-tools.js +147 -0
- package/build/tools/handlers/active-directory-handler.js +321 -0
- package/build/tools/handlers/backup-handler.js +451 -0
- package/build/tools/handlers/backup-policy-handler.js +275 -0
- package/build/tools/handlers/backup-vault-handler.js +370 -0
- package/build/tools/handlers/kms-config-handler.js +327 -0
- package/build/tools/handlers/operation-handler.js +254 -0
- package/build/tools/handlers/quota-rule-handler.js +411 -0
- package/build/tools/handlers/replication-handler.js +504 -0
- package/build/tools/handlers/snapshot-handler.js +320 -0
- package/build/tools/handlers/storage-pool-handler.js +346 -0
- package/build/tools/handlers/volume-handler.js +353 -0
- package/build/tools/kms-config-tools.js +162 -0
- package/build/tools/operation-tools.js +64 -0
- package/build/tools/quota-rule-tools.js +166 -0
- package/build/tools/replication-tools.js +227 -0
- package/build/tools/snapshot-tools.js +124 -0
- package/build/tools/storage-pool-tools.js +215 -0
- package/build/tools/volume-tools.js +216 -0
- package/build/types/tool.js +1 -0
- package/build/utils/netapp-client-factory.js +53 -0
- package/gemini-extension.json +12 -0
- package/package.json +66 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { NetAppClientFactory } from '../../utils/netapp-client-factory.js';
|
|
2
|
+
import { logger } from '../../logger.js';
|
|
3
|
+
const log = logger.child({ module: 'quota-rule-handler' });
|
|
4
|
+
// Basic runtime validation so we fail fast before calling the NetApp API
|
|
5
|
+
function validatePathArgs(args, requireQuotaRuleId = false) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
const required = [
|
|
8
|
+
{ key: 'projectId', value: args.projectId },
|
|
9
|
+
{ key: 'location', value: args.location },
|
|
10
|
+
{ key: 'volumeId', value: args.volumeId },
|
|
11
|
+
];
|
|
12
|
+
if (requireQuotaRuleId) {
|
|
13
|
+
required.push({ key: 'quotaRuleId', value: args.quotaRuleId });
|
|
14
|
+
}
|
|
15
|
+
for (const field of required) {
|
|
16
|
+
if (typeof field.value !== 'string' || field.value.trim() === '') {
|
|
17
|
+
errors.push(`Missing or invalid ${field.key}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return errors;
|
|
21
|
+
}
|
|
22
|
+
function parseOptionalNumber(value, fieldName) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return {};
|
|
25
|
+
const num = Number(value);
|
|
26
|
+
if (!Number.isFinite(num) || num < 0) {
|
|
27
|
+
return { error: `${fieldName} must be a non-negative number` };
|
|
28
|
+
}
|
|
29
|
+
return { value: num };
|
|
30
|
+
}
|
|
31
|
+
// Helper to format quota rule data
|
|
32
|
+
function formatQuotaRuleData(rule) {
|
|
33
|
+
const result = {};
|
|
34
|
+
if (!rule)
|
|
35
|
+
return result;
|
|
36
|
+
if (rule.name) {
|
|
37
|
+
const nameParts = rule.name.split('/');
|
|
38
|
+
result.name = rule.name;
|
|
39
|
+
result.quotaRuleId = nameParts[nameParts.length - 1];
|
|
40
|
+
}
|
|
41
|
+
if (rule.target)
|
|
42
|
+
result.target = rule.target;
|
|
43
|
+
if (rule.type) {
|
|
44
|
+
result.type = rule.type;
|
|
45
|
+
result.quotaType = rule.type;
|
|
46
|
+
}
|
|
47
|
+
else if (rule.quotaType) {
|
|
48
|
+
// fallback if API ever returns legacy field name
|
|
49
|
+
result.quotaType = rule.quotaType;
|
|
50
|
+
result.type = rule.quotaType;
|
|
51
|
+
}
|
|
52
|
+
if (rule.diskLimitMib !== undefined) {
|
|
53
|
+
const mib = Number(rule.diskLimitMib);
|
|
54
|
+
result.diskLimitMib = mib;
|
|
55
|
+
}
|
|
56
|
+
if (rule.state)
|
|
57
|
+
result.state = rule.state;
|
|
58
|
+
if (rule.createTime) {
|
|
59
|
+
result.createTime = new Date(rule.createTime.seconds * 1000);
|
|
60
|
+
}
|
|
61
|
+
if (rule.description)
|
|
62
|
+
result.description = rule.description;
|
|
63
|
+
if (rule.labels)
|
|
64
|
+
result.labels = rule.labels;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// Validate and normalize quota rule type to numeric enum value
|
|
68
|
+
function parseQuotaType(input) {
|
|
69
|
+
const enumMap = {
|
|
70
|
+
TYPE_UNSPECIFIED: 0,
|
|
71
|
+
INDIVIDUAL_USER_QUOTA: 1,
|
|
72
|
+
INDIVIDUAL_GROUP_QUOTA: 2,
|
|
73
|
+
DEFAULT_USER_QUOTA: 3,
|
|
74
|
+
DEFAULT_GROUP_QUOTA: 4,
|
|
75
|
+
};
|
|
76
|
+
if (input === undefined || input === null)
|
|
77
|
+
return {};
|
|
78
|
+
// Accept numeric enum values directly
|
|
79
|
+
if (typeof input === 'number') {
|
|
80
|
+
if (Object.values(enumMap).includes(input)) {
|
|
81
|
+
return { value: input };
|
|
82
|
+
}
|
|
83
|
+
return { error: 'quotaType/type must be a valid enum number (0-4)' };
|
|
84
|
+
}
|
|
85
|
+
if (typeof input === 'string') {
|
|
86
|
+
const trimmed = input.trim().toUpperCase();
|
|
87
|
+
if (enumMap[trimmed] !== undefined) {
|
|
88
|
+
return { value: enumMap[trimmed] };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
error: 'quotaType/type must be one of TYPE_UNSPECIFIED, INDIVIDUAL_USER_QUOTA, INDIVIDUAL_GROUP_QUOTA, DEFAULT_USER_QUOTA, DEFAULT_GROUP_QUOTA, or the corresponding enum number',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { error: 'quotaType/type must be a string enum name or enum number' };
|
|
95
|
+
}
|
|
96
|
+
// Create Quota Rule Handler
|
|
97
|
+
export const createQuotaRuleHandler = async (args) => {
|
|
98
|
+
try {
|
|
99
|
+
const { projectId, location, volumeId, quotaRuleId, target, quotaType, type, diskLimitMib, description, labels, } = args;
|
|
100
|
+
const errors = [
|
|
101
|
+
...validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true),
|
|
102
|
+
];
|
|
103
|
+
if (!target || typeof target !== 'string' || target.trim() === '') {
|
|
104
|
+
errors.push('target is required');
|
|
105
|
+
}
|
|
106
|
+
const { value: parsedType, error: typeError } = parseQuotaType(type ?? quotaType);
|
|
107
|
+
if (typeError)
|
|
108
|
+
errors.push(typeError);
|
|
109
|
+
if (parsedType === undefined)
|
|
110
|
+
errors.push('quotaType/type is required');
|
|
111
|
+
const { value: parsedDiskLimitMib, error: diskLimitError } = parseOptionalNumber(diskLimitMib, 'diskLimitMib');
|
|
112
|
+
if (diskLimitError)
|
|
113
|
+
errors.push(diskLimitError);
|
|
114
|
+
if (parsedDiskLimitMib === undefined)
|
|
115
|
+
errors.push('diskLimitMib is required');
|
|
116
|
+
if (errors.length > 0) {
|
|
117
|
+
return {
|
|
118
|
+
isError: true,
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: `Invalid input: ${errors.join('; ')}`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const netAppClient = NetAppClientFactory.createClient();
|
|
128
|
+
const parent = `projects/${projectId}/locations/${location}/volumes/${volumeId}`;
|
|
129
|
+
const quotaRule = {};
|
|
130
|
+
if (target)
|
|
131
|
+
quotaRule.target = target;
|
|
132
|
+
if (parsedType !== undefined)
|
|
133
|
+
quotaRule.type = parsedType;
|
|
134
|
+
if (parsedDiskLimitMib !== undefined)
|
|
135
|
+
quotaRule.diskLimitMib = parsedDiskLimitMib;
|
|
136
|
+
if (description)
|
|
137
|
+
quotaRule.description = description;
|
|
138
|
+
if (labels)
|
|
139
|
+
quotaRule.labels = labels;
|
|
140
|
+
const request = {
|
|
141
|
+
parent,
|
|
142
|
+
quotaRuleId,
|
|
143
|
+
quotaRule,
|
|
144
|
+
};
|
|
145
|
+
log.info({ request }, 'Create Quota Rule request');
|
|
146
|
+
const [operation] = await netAppClient.createQuotaRule(request);
|
|
147
|
+
log.info({ operation }, 'Create Quota Rule operation');
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: JSON.stringify({
|
|
153
|
+
name: `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`,
|
|
154
|
+
operation: operation,
|
|
155
|
+
}, null, 2),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
structuredContent: {
|
|
159
|
+
name: `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`,
|
|
160
|
+
operationId: operation.name || '',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
log.error({ err: error }, 'Error creating quota rule');
|
|
166
|
+
return {
|
|
167
|
+
isError: true,
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: `Error creating quota rule: ${error.message || 'Unknown error'}`,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// Delete Quota Rule Handler
|
|
178
|
+
export const deleteQuotaRuleHandler = async (args) => {
|
|
179
|
+
try {
|
|
180
|
+
const { projectId, location, volumeId, quotaRuleId } = args;
|
|
181
|
+
const errors = validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true);
|
|
182
|
+
if (errors.length > 0) {
|
|
183
|
+
return {
|
|
184
|
+
isError: true,
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: `Invalid input: ${errors.join('; ')}`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const netAppClient = NetAppClientFactory.createClient();
|
|
194
|
+
const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
|
|
195
|
+
const [operation] = await netAppClient.deleteQuotaRule({ name });
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: JSON.stringify({
|
|
201
|
+
message: `Quota rule ${quotaRuleId} deletion requested`,
|
|
202
|
+
operation: operation,
|
|
203
|
+
}, null, 2),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
structuredContent: {
|
|
207
|
+
success: true,
|
|
208
|
+
operationId: operation.name || '',
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
log.error({ err: error }, 'Error deleting quota rule');
|
|
214
|
+
return {
|
|
215
|
+
isError: true,
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Error deleting quota rule: ${error.message || 'Unknown error'}`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
// Get Quota Rule Handler
|
|
226
|
+
export const getQuotaRuleHandler = async (args) => {
|
|
227
|
+
try {
|
|
228
|
+
const { projectId, location, volumeId, quotaRuleId } = args;
|
|
229
|
+
const errors = validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true);
|
|
230
|
+
if (errors.length > 0) {
|
|
231
|
+
return {
|
|
232
|
+
isError: true,
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: `Invalid input: ${errors.join('; ')}`,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const netAppClient = NetAppClientFactory.createClient();
|
|
242
|
+
const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
|
|
243
|
+
const [quotaRule] = await netAppClient.getQuotaRule({ name });
|
|
244
|
+
const formatted = formatQuotaRuleData(quotaRule);
|
|
245
|
+
return {
|
|
246
|
+
content: [
|
|
247
|
+
{
|
|
248
|
+
type: 'text',
|
|
249
|
+
text: JSON.stringify(formatted, null, 2),
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
structuredContent: formatted,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
log.error({ err: error }, 'Error getting quota rule');
|
|
257
|
+
return {
|
|
258
|
+
isError: true,
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: 'text',
|
|
262
|
+
text: `Error getting quota rule: ${error.message || 'Unknown error'}`,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
// List Quota Rules Handler
|
|
269
|
+
export const listQuotaRulesHandler = async (args) => {
|
|
270
|
+
try {
|
|
271
|
+
const { projectId, location, volumeId, filter, pageSize, pageToken, orderBy } = args;
|
|
272
|
+
const errors = validatePathArgs({ projectId, location, volumeId }, false);
|
|
273
|
+
const { value: parsedPageSize, error: pageSizeError } = parseOptionalNumber(pageSize, 'pageSize');
|
|
274
|
+
if (pageSizeError)
|
|
275
|
+
errors.push(pageSizeError);
|
|
276
|
+
if (errors.length > 0) {
|
|
277
|
+
return {
|
|
278
|
+
isError: true,
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: `Invalid input: ${errors.join('; ')}`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const netAppClient = NetAppClientFactory.createClient();
|
|
288
|
+
const parent = `projects/${projectId}/locations/${location}/volumes/${volumeId}`;
|
|
289
|
+
const request = { parent };
|
|
290
|
+
if (filter)
|
|
291
|
+
request.filter = filter;
|
|
292
|
+
if (parsedPageSize !== undefined)
|
|
293
|
+
request.pageSize = parsedPageSize;
|
|
294
|
+
if (pageToken)
|
|
295
|
+
request.pageToken = pageToken;
|
|
296
|
+
if (orderBy)
|
|
297
|
+
request.orderBy = orderBy;
|
|
298
|
+
const [quotaRules, , response] = await netAppClient.listQuotaRules(request);
|
|
299
|
+
const formatted = quotaRules.map(formatQuotaRuleData);
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
text: JSON.stringify({ quotaRules, nextPageToken: response?.nextPageToken }, null, 2),
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
structuredContent: {
|
|
308
|
+
quotaRules: formatted,
|
|
309
|
+
nextPageToken: response?.nextPageToken,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
log.error({ err: error }, 'Error listing quota rules');
|
|
315
|
+
return {
|
|
316
|
+
isError: true,
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: `Error listing quota rules: ${error.message || 'Unknown error'}`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// Update Quota Rule Handler
|
|
327
|
+
export const updateQuotaRuleHandler = async (args) => {
|
|
328
|
+
try {
|
|
329
|
+
const { projectId, location, volumeId, quotaRuleId, target, quotaType, type, diskLimitMib, description, labels, } = args;
|
|
330
|
+
const errors = [
|
|
331
|
+
...validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true),
|
|
332
|
+
];
|
|
333
|
+
const { value: parsedDiskLimitMib, error: diskLimitError } = parseOptionalNumber(diskLimitMib, 'diskLimitMib');
|
|
334
|
+
if (diskLimitError)
|
|
335
|
+
errors.push(diskLimitError);
|
|
336
|
+
const { value: parsedType, error: typeError } = parseQuotaType(type ?? quotaType);
|
|
337
|
+
if (typeError)
|
|
338
|
+
errors.push(typeError);
|
|
339
|
+
const netAppClient = NetAppClientFactory.createClient();
|
|
340
|
+
const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
|
|
341
|
+
const updateMask = [];
|
|
342
|
+
const quotaRule = { name };
|
|
343
|
+
if (target !== undefined) {
|
|
344
|
+
quotaRule.target = target;
|
|
345
|
+
updateMask.push('target');
|
|
346
|
+
}
|
|
347
|
+
if (parsedType !== undefined) {
|
|
348
|
+
quotaRule.type = parsedType;
|
|
349
|
+
updateMask.push('type');
|
|
350
|
+
}
|
|
351
|
+
if (parsedDiskLimitMib !== undefined) {
|
|
352
|
+
quotaRule.diskLimitMib = parsedDiskLimitMib;
|
|
353
|
+
updateMask.push('disk_limit_mib');
|
|
354
|
+
}
|
|
355
|
+
if (description !== undefined) {
|
|
356
|
+
quotaRule.description = description;
|
|
357
|
+
updateMask.push('description');
|
|
358
|
+
}
|
|
359
|
+
if (labels !== undefined) {
|
|
360
|
+
quotaRule.labels = labels;
|
|
361
|
+
updateMask.push('labels');
|
|
362
|
+
}
|
|
363
|
+
if (updateMask.length === 0) {
|
|
364
|
+
errors.push('Provide at least one field to update');
|
|
365
|
+
}
|
|
366
|
+
if (errors.length > 0) {
|
|
367
|
+
return {
|
|
368
|
+
isError: true,
|
|
369
|
+
content: [
|
|
370
|
+
{
|
|
371
|
+
type: 'text',
|
|
372
|
+
text: `Invalid input: ${errors.join('; ')}`,
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const request = {
|
|
378
|
+
quotaRule,
|
|
379
|
+
updateMask: updateMask.length > 0 ? { paths: updateMask } : undefined,
|
|
380
|
+
};
|
|
381
|
+
log.info({ request }, 'Update Quota Rule request');
|
|
382
|
+
const [operation] = await netAppClient.updateQuotaRule(request);
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: 'text',
|
|
387
|
+
text: JSON.stringify({
|
|
388
|
+
message: `Quota rule ${quotaRuleId} update requested`,
|
|
389
|
+
operation: operation,
|
|
390
|
+
}, null, 2),
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
structuredContent: {
|
|
394
|
+
name: name,
|
|
395
|
+
operationId: operation.name || '',
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
log.error({ err: error }, 'Error updating quota rule');
|
|
401
|
+
return {
|
|
402
|
+
isError: true,
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: `Error updating quota rule: ${error.message || 'Unknown error'}`,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
};
|