vme-mcp-server 0.1.5 → 0.1.6

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.
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getResources = exports.resolveInput = exports.getClusterNodes = exports.api = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ // Setup axios instance for VME API
9
+ exports.api = axios_1.default.create({
10
+ baseURL: process.env.VME_API_BASE_URL,
11
+ headers: {
12
+ Authorization: `Bearer ${process.env.VME_API_TOKEN}`,
13
+ "Content-Type": "application/json"
14
+ }
15
+ });
16
+ // Helper function to get available cluster nodes
17
+ async function getClusterNodes() {
18
+ try {
19
+ const clusters = await exports.api.get("/clusters");
20
+ const prodCluster = clusters.data.clusters.find((cluster) => cluster.name === "prod01");
21
+ if (prodCluster && prodCluster.servers) {
22
+ return prodCluster.servers.map((server) => server.id);
23
+ }
24
+ }
25
+ catch (error) {
26
+ console.error('Error getting cluster nodes:', error);
27
+ }
28
+ // Fallback to known nodes if API fails
29
+ return [1, 2, 3];
30
+ }
31
+ exports.getClusterNodes = getClusterNodes;
32
+ // Resolve user input parameters to VME API resource IDs
33
+ async function resolveInput({ group, cloud, template, size }) {
34
+ const [groups, zones, instanceTypes, servicePlans] = await Promise.all([
35
+ exports.api.get("/groups"),
36
+ exports.api.get("/zones"),
37
+ exports.api.get("/instance-types"),
38
+ exports.api.get("/service-plans")
39
+ ]);
40
+ // Find group (case-insensitive)
41
+ const foundGroup = groups.data.groups.find((g) => g.name.toLowerCase() === group.toLowerCase());
42
+ // Find zone/cloud (case-insensitive, handle both terms)
43
+ const foundZone = zones.data.zones.find((z) => z.name.toLowerCase() === cloud.toLowerCase());
44
+ // In HPE VM Essentials, all VMs use "HPE VM" instance type
45
+ // The actual OS is determined by the image selection
46
+ const instanceType = instanceTypes.data.instanceTypes.find((t) => t.name === 'HPE VM');
47
+ // Find appropriate image based on template using osType properties
48
+ let selectedImage;
49
+ const templateLower = template.toLowerCase();
50
+ // Get available images to find the right one
51
+ const images = await exports.api.get("/virtual-images");
52
+ if (templateLower.includes('ubuntu')) {
53
+ // Find Ubuntu images using osType.category and prefer latest version
54
+ const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
55
+ // Sort by version descending (24.04 > 22.04 > 20.04)
56
+ const aVersion = parseFloat(a.osType.osVersion || '0');
57
+ const bVersion = parseFloat(b.osType.osVersion || '0');
58
+ return bVersion - aVersion;
59
+ });
60
+ selectedImage = ubuntuImages[0];
61
+ }
62
+ else if (templateLower.includes('rocky')) {
63
+ // Find Rocky Linux images and prefer latest version (Rocky 9 > Rocky 8)
64
+ const rockyImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rocky').sort((a, b) => {
65
+ const aVersion = parseInt(a.osType.osVersion || '0');
66
+ const bVersion = parseInt(b.osType.osVersion || '0');
67
+ return bVersion - aVersion; // Latest first
68
+ });
69
+ selectedImage = rockyImages[0];
70
+ }
71
+ else if (templateLower.includes('centos')) {
72
+ // Find CentOS images and prefer latest version (CentOS 9 > CentOS 8)
73
+ const centosImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'centos').sort((a, b) => {
74
+ const aVersion = parseInt(a.osType.osVersion || '0');
75
+ const bVersion = parseInt(b.osType.osVersion || '0');
76
+ return bVersion - aVersion; // Latest first
77
+ });
78
+ selectedImage = centosImages[0];
79
+ }
80
+ else if (templateLower.includes('debian')) {
81
+ // Find Debian images and prefer latest version (Debian 12 > Debian 11)
82
+ const debianImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'debian').sort((a, b) => {
83
+ const aVersion = parseInt(a.osType.osVersion || '0');
84
+ const bVersion = parseInt(b.osType.osVersion || '0');
85
+ return bVersion - aVersion; // Latest first
86
+ });
87
+ selectedImage = debianImages[0];
88
+ }
89
+ else if (templateLower.includes('alma')) {
90
+ // Find AlmaLinux images and prefer latest version (AlmaLinux 9 > AlmaLinux 8)
91
+ const almaImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'almalinux').sort((a, b) => {
92
+ const aVersion = parseInt(a.osType.osVersion || '0');
93
+ const bVersion = parseInt(b.osType.osVersion || '0');
94
+ return bVersion - aVersion; // Latest first
95
+ });
96
+ selectedImage = almaImages[0];
97
+ }
98
+ else if (templateLower.includes('rhel') || templateLower.includes('red hat')) {
99
+ // Find RHEL images and prefer latest version
100
+ const rhelImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rhel').sort((a, b) => {
101
+ const aVersion = parseInt(a.osType.osVersion || '0');
102
+ const bVersion = parseInt(b.osType.osVersion || '0');
103
+ return bVersion - aVersion; // Latest first
104
+ });
105
+ selectedImage = rhelImages[0];
106
+ }
107
+ // Default to latest Ubuntu if no specific match
108
+ if (!selectedImage) {
109
+ const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
110
+ const aVersion = parseFloat(a.osType.osVersion || '0');
111
+ const bVersion = parseFloat(b.osType.osVersion || '0');
112
+ return bVersion - aVersion;
113
+ });
114
+ selectedImage = ubuntuImages[0] || images.data.virtualImages[0];
115
+ }
116
+ // Find service plan based on size (flexible matching)
117
+ let plan;
118
+ const sizeLower = size.toLowerCase();
119
+ if (sizeLower.includes('8gb') || sizeLower.includes('8 gb') || sizeLower.includes('medium')) {
120
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('2 CPU, 8GB Memory'));
121
+ }
122
+ else if (sizeLower.includes('4gb') || sizeLower.includes('4 gb') || sizeLower.includes('small')) {
123
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('1 CPU, 4GB Memory'));
124
+ }
125
+ // Fallback to any available plan
126
+ if (!plan) {
127
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('CPU') && p.name.includes('Memory'));
128
+ }
129
+ return {
130
+ groupId: foundGroup?.id || groups.data.groups[0]?.id,
131
+ cloudId: foundZone?.id || zones.data.zones[0]?.id,
132
+ instanceTypeId: instanceType?.id,
133
+ servicePlanId: plan?.id,
134
+ imageId: selectedImage?.id,
135
+ // Return resolved names for better error reporting
136
+ resolvedGroup: foundGroup?.name || groups.data.groups[0]?.name,
137
+ resolvedCloud: foundZone?.name || zones.data.zones[0]?.name,
138
+ resolvedInstanceType: instanceType?.name,
139
+ resolvedPlan: plan?.name,
140
+ resolvedImage: selectedImage?.osType?.name || selectedImage?.name,
141
+ // Available options for error messages
142
+ availableGroups: groups.data.groups.map((g) => g.name),
143
+ availableZones: zones.data.zones.map((z) => z.name)
144
+ };
145
+ }
146
+ exports.resolveInput = resolveInput;
147
+ // Unified resource discovery function
148
+ async function getResources(type, intent, role) {
149
+ try {
150
+ // Fetch all resource types in parallel for comprehensive discovery
151
+ const [groups, zones, instanceTypes, servicePlans, virtualImages, clusters] = await Promise.all([
152
+ exports.api.get("/groups"),
153
+ exports.api.get("/zones"),
154
+ exports.api.get("/instance-types"),
155
+ exports.api.get("/service-plans"),
156
+ exports.api.get("/virtual-images"),
157
+ exports.api.get("/clusters")
158
+ ]);
159
+ const allResources = {
160
+ groups: groups.data.groups || [],
161
+ zones: zones.data.zones || [],
162
+ instanceTypes: instanceTypes.data.instanceTypes || [],
163
+ servicePlans: servicePlans.data.servicePlans || [],
164
+ virtualImages: virtualImages.data.virtualImages || [],
165
+ clusters: clusters.data.clusters || []
166
+ };
167
+ // Apply intelligent filtering based on type
168
+ if (type) {
169
+ const typeLower = type.toLowerCase();
170
+ if (typeLower.includes('compute') || typeLower.includes('vm')) {
171
+ return {
172
+ groups: allResources.groups,
173
+ zones: allResources.zones,
174
+ servicePlans: allResources.servicePlans,
175
+ virtualImages: allResources.virtualImages,
176
+ clusters: allResources.clusters,
177
+ _filtered_for: 'compute/vm resources'
178
+ };
179
+ }
180
+ else if (typeLower.includes('network')) {
181
+ return {
182
+ zones: allResources.zones,
183
+ clusters: allResources.clusters,
184
+ _filtered_for: 'network resources'
185
+ };
186
+ }
187
+ else if (typeLower.includes('storage')) {
188
+ return {
189
+ servicePlans: allResources.servicePlans,
190
+ zones: allResources.zones,
191
+ _filtered_for: 'storage resources'
192
+ };
193
+ }
194
+ }
195
+ // Apply intent-based filtering
196
+ if (intent) {
197
+ const intentLower = intent.toLowerCase();
198
+ if (intentLower.includes('create') || intentLower.includes('provision')) {
199
+ // For creation intents, focus on templates and plans
200
+ return {
201
+ groups: allResources.groups,
202
+ zones: allResources.zones,
203
+ servicePlans: allResources.servicePlans,
204
+ virtualImages: allResources.virtualImages.map((img) => ({
205
+ id: img.id,
206
+ name: img.name,
207
+ osType: img.osType,
208
+ recommended: img.osType?.category === 'ubuntu' ? true : false
209
+ })),
210
+ _filtered_for: 'creation/provisioning intent'
211
+ };
212
+ }
213
+ else if (intentLower.includes('list') || intentLower.includes('show') || intentLower.includes('discover')) {
214
+ // For discovery intents, return comprehensive view
215
+ return {
216
+ summary: {
217
+ total_groups: allResources.groups.length,
218
+ total_zones: allResources.zones.length,
219
+ total_service_plans: allResources.servicePlans.length,
220
+ total_os_templates: allResources.virtualImages.length,
221
+ total_clusters: allResources.clusters.length
222
+ },
223
+ ...allResources,
224
+ _filtered_for: 'discovery/listing intent'
225
+ };
226
+ }
227
+ }
228
+ // Default: return all resources with semantic organization
229
+ return {
230
+ compute: {
231
+ groups: allResources.groups,
232
+ servicePlans: allResources.servicePlans,
233
+ virtualImages: allResources.virtualImages
234
+ },
235
+ infrastructure: {
236
+ zones: allResources.zones,
237
+ clusters: allResources.clusters,
238
+ instanceTypes: allResources.instanceTypes
239
+ },
240
+ _organization: 'semantic grouping by function'
241
+ };
242
+ }
243
+ catch (error) {
244
+ return {
245
+ error: `Failed to fetch resources: ${error.response?.data?.message || error.message}`,
246
+ available_endpoints: ['/groups', '/zones', '/service-plans', '/virtual-images', '/clusters'],
247
+ fallback_suggestion: 'Try checking individual resource types with specific API endpoints'
248
+ };
249
+ }
250
+ }
251
+ exports.getResources = getResources;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVMIntent = void 0;
4
+ const vm_parsing_js_1 = require("./vm-parsing.js");
5
+ // Intent recognition system for natural language processing
6
+ function parseVMIntent(naturalLanguageInput) {
7
+ const input = naturalLanguageInput.toLowerCase().trim();
8
+ const result = {
9
+ action: 'unknown',
10
+ confidence: 0,
11
+ entities: {},
12
+ originalText: naturalLanguageInput
13
+ };
14
+ // Intent Classification with confidence scoring
15
+ const intentPatterns = {
16
+ create: [
17
+ /\b(create|provision|deploy|launch|start|build|make|setup)\b/,
18
+ /\bneed\s+(\d+\s+)?(vm|server|machine)/,
19
+ /\bcan\s+you\s+(create|provision|deploy)/
20
+ ],
21
+ modify: [
22
+ /\b(modify|change|update|edit|alter|resize|scale)\b/,
23
+ /\badd\s+(more\s+)?(cpu|memory|disk|storage)/,
24
+ /\bchange\s+the\s+(size|plan|configuration)/
25
+ ],
26
+ monitor: [
27
+ /\b(show|list|display|check|status|monitor|view)\b(?!.*\b(available|options|resources|templates|plans)\b)/,
28
+ /\bwhat\s+(vm|server|machine)/,
29
+ /\bhow\s+many\s+(vm|server|machine)/
30
+ ],
31
+ destroy: [
32
+ /\b(delete|remove|destroy|terminate|stop|kill)\b/,
33
+ /\btear\s+down/,
34
+ /\bshut\s+down/
35
+ ],
36
+ discover: [
37
+ /\b(available|options|resources|templates|plans)\b/,
38
+ /\bshow\s+me\s+(all|available)/,
39
+ /\bwhat\s+(are|is)\s+(available|possible)/,
40
+ /\bwhat\s+can\s+i\s+(create|do)/
41
+ ]
42
+ };
43
+ // Calculate confidence for each intent
44
+ let maxConfidence = 0;
45
+ let detectedAction = 'unknown';
46
+ for (const [action, patterns] of Object.entries(intentPatterns)) {
47
+ let matches = 0;
48
+ for (const pattern of patterns) {
49
+ if (pattern.test(input)) {
50
+ matches++;
51
+ }
52
+ }
53
+ const confidence = patterns.length > 0 ? matches / patterns.length : 0;
54
+ if (confidence > maxConfidence) {
55
+ maxConfidence = confidence;
56
+ detectedAction = action;
57
+ }
58
+ }
59
+ result.action = detectedAction;
60
+ result.confidence = maxConfidence;
61
+ // Entity Extraction for VM creation
62
+ if (result.action === 'create') {
63
+ // Extract VM names and count
64
+ const namePatterns = [
65
+ /\b(?:vm|server|machine)s?\s+(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
66
+ /\b(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?\s+(?:vm|server|machine)/,
67
+ /\bcreate\s+(?:(?:vm|server|machine)s?\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
68
+ /\b(?:vm|server|machine)\s+named\s+["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/
69
+ ];
70
+ for (const pattern of namePatterns) {
71
+ const match = input.match(pattern);
72
+ if (match) {
73
+ const nameInput = match[1] || match[2];
74
+ result.entities.vmNames = (0, vm_parsing_js_1.parseVMNames)(nameInput);
75
+ break;
76
+ }
77
+ }
78
+ // Extract count
79
+ const countMatch = input.match(/\b(\d+)\s+(?:vm|server|machine)/);
80
+ if (countMatch) {
81
+ result.entities.count = parseInt(countMatch[1]);
82
+ }
83
+ // Extract OS template
84
+ const osPatterns = [
85
+ /\b(ubuntu|rocky|centos|debian|rhel|red\s*hat|alma|almalinux)(?:\s+(\d+(?:\.\d+)?))?/,
86
+ /\busing\s+([a-zA-Z0-9\-_]+)\s+(?:template|os|operating)/,
87
+ /\b(?:template|os|operating)\s+([a-zA-Z0-9\-_]+)/
88
+ ];
89
+ for (const pattern of osPatterns) {
90
+ const match = input.match(pattern);
91
+ if (match) {
92
+ result.entities.osTemplate = match[0];
93
+ break;
94
+ }
95
+ }
96
+ // Extract size/memory
97
+ const sizePatterns = [
98
+ /\b(small|medium|large|tiny)\b/,
99
+ /\b(\d+)\s*gb\s+(?:memory|ram)\b/,
100
+ /\b(\d+)\s*cpu[s]?\s*,?\s*(\d+)\s*gb/,
101
+ /\bwith\s+(\d+(?:gb|cpu))\b/,
102
+ /\b(\d+gb)\b/
103
+ ];
104
+ for (const pattern of sizePatterns) {
105
+ const match = input.match(pattern);
106
+ if (match) {
107
+ result.entities.size = match[0];
108
+ break;
109
+ }
110
+ }
111
+ // Extract group/environment
112
+ const groupPatterns = [
113
+ /\bin\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+(?:group|environment|site)/,
114
+ /\b(?:group|environment|site)\s+([a-zA-Z0-9\-_]+)/,
115
+ /\bfor\s+([a-zA-Z0-9\-_]+)\s+(?:environment|testing|production)/
116
+ ];
117
+ for (const pattern of groupPatterns) {
118
+ const match = input.match(pattern);
119
+ if (match) {
120
+ result.entities.group = match[1];
121
+ break;
122
+ }
123
+ }
124
+ // Extract zone/cloud
125
+ const zonePatterns = [
126
+ /\bon\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
127
+ /\b(?:zone|cloud)\s+([a-zA-Z0-9\-_]+)/,
128
+ /\bin\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
129
+ /\b(?:deploy|launch|create)\s+(?:in\s+)?([a-zA-Z0-9\-_]+)\s+zone/
130
+ ];
131
+ for (const pattern of zonePatterns) {
132
+ const match = input.match(pattern);
133
+ if (match) {
134
+ result.entities.zone = match[1];
135
+ break;
136
+ }
137
+ }
138
+ // Extract distribution strategy
139
+ if (input.includes('spread') || input.includes('distribute') || input.includes('across')) {
140
+ result.entities.distribution = 'spread';
141
+ }
142
+ else if (input.includes('each node') || input.includes('all nodes')) {
143
+ result.entities.distribution = 'spread';
144
+ }
145
+ else if (input.includes('node') && input.match(/node[s]?\s*[\d,\s]+/)) {
146
+ const nodeMatch = input.match(/node[s]?\s*([\d,\s]+)/);
147
+ if (nodeMatch) {
148
+ result.entities.distribution = nodeMatch[1].replace(/\s+/g, '');
149
+ }
150
+ }
151
+ // Boost confidence if we extracted entities
152
+ const entityCount = Object.keys(result.entities).length;
153
+ if (entityCount > 0) {
154
+ result.confidence = Math.min(0.95, result.confidence + (entityCount * 0.1));
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ exports.parseVMIntent = parseVMIntent;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logInteraction = exports.generateSessionId = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ // AI Training Data Collection Configuration
7
+ const AI_TRAINING_ENABLED = process.env.ENABLE_AI_TRAINING_DATA === 'true';
8
+ const AI_TRAINING_FIELDS = (process.env.AI_TRAINING_DATA_FIELDS || 'user_input,parsed_output,success_metrics').split(',');
9
+ const AI_TRAINING_RETENTION_DAYS = parseInt(process.env.AI_TRAINING_DATA_RETENTION_DAYS || '30');
10
+ // Generate session ID for interaction tracking
11
+ function generateSessionId() {
12
+ return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
13
+ }
14
+ exports.generateSessionId = generateSessionId;
15
+ // Privacy-aware interaction logging
16
+ function logInteraction(interaction) {
17
+ if (!AI_TRAINING_ENABLED) {
18
+ return; // Respect user privacy choice
19
+ }
20
+ try {
21
+ // Ensure logs directory exists
22
+ const logsDir = (0, path_1.join)(process.cwd(), 'ai-training-logs');
23
+ if (!(0, fs_1.existsSync)(logsDir)) {
24
+ (0, fs_1.mkdirSync)(logsDir, { recursive: true });
25
+ }
26
+ // Filter fields based on user preferences
27
+ const filteredInteraction = {
28
+ id: interaction.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
29
+ timestamp: new Date().toISOString(),
30
+ session_id: interaction.session_id || 'unknown',
31
+ tool_name: interaction.tool_name || 'unknown'
32
+ };
33
+ // Only include fields user has opted into
34
+ if (AI_TRAINING_FIELDS.includes('user_input') && interaction.user_input) {
35
+ filteredInteraction.user_input = interaction.user_input;
36
+ }
37
+ if (AI_TRAINING_FIELDS.includes('parsed_output') && interaction.parsed_output) {
38
+ filteredInteraction.parsed_output = interaction.parsed_output;
39
+ }
40
+ if (AI_TRAINING_FIELDS.includes('api_calls') && interaction.api_calls) {
41
+ filteredInteraction.api_calls = interaction.api_calls;
42
+ }
43
+ if (AI_TRAINING_FIELDS.includes('success_metrics') && interaction.success_metrics) {
44
+ filteredInteraction.success_metrics = interaction.success_metrics;
45
+ }
46
+ if (AI_TRAINING_FIELDS.includes('timing') && interaction.timing) {
47
+ filteredInteraction.timing = interaction.timing;
48
+ }
49
+ // Append to daily log file (JSONL format)
50
+ const logFile = (0, path_1.join)(logsDir, `interactions-${new Date().toISOString().split('T')[0]}.jsonl`);
51
+ const logEntry = JSON.stringify(filteredInteraction) + '\n';
52
+ (0, fs_1.writeFileSync)(logFile, logEntry, { flag: 'a' });
53
+ }
54
+ catch (error) {
55
+ // Fail silently to not disrupt user experience
56
+ console.warn('AI training data logging failed:', error);
57
+ }
58
+ }
59
+ exports.logInteraction = logInteraction;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateNodeAssignments = exports.parseVMNames = void 0;
4
+ // Helper function to parse VM name patterns
5
+ function parseVMNames(nameInput, count) {
6
+ // Handle range patterns like "web01->web03" or "web01 -> web03"
7
+ const rangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
8
+ if (rangeMatch) {
9
+ const [, prefix1, start, prefix2, end] = rangeMatch;
10
+ if (prefix1.trim() === prefix2.trim()) {
11
+ const basePrefix = prefix1.trim();
12
+ const startNum = parseInt(start);
13
+ const endNum = parseInt(end);
14
+ const names = [];
15
+ for (let i = startNum; i <= endNum; i++) {
16
+ const paddedNum = i.toString().padStart(start.length, '0');
17
+ names.push(`${basePrefix}${paddedNum}`);
18
+ }
19
+ return names;
20
+ }
21
+ }
22
+ // Handle count-based generation
23
+ if (count && count > 1) {
24
+ const names = [];
25
+ // Extract base name and number pattern
26
+ const match = nameInput.match(/^(.+?)(\d+)$/);
27
+ if (match) {
28
+ const [, prefix, startStr] = match;
29
+ const startNum = parseInt(startStr);
30
+ for (let i = 0; i < count; i++) {
31
+ const num = startNum + i;
32
+ const paddedNum = num.toString().padStart(startStr.length, '0');
33
+ names.push(`${prefix}${paddedNum}`);
34
+ }
35
+ return names;
36
+ }
37
+ else {
38
+ // If no number pattern, append numbers
39
+ for (let i = 1; i <= count; i++) {
40
+ const paddedNum = i.toString().padStart(2, '0');
41
+ names.push(`${nameInput}${paddedNum}`);
42
+ }
43
+ return names;
44
+ }
45
+ }
46
+ // Single VM
47
+ return [nameInput];
48
+ }
49
+ exports.parseVMNames = parseVMNames;
50
+ // Determine node assignment strategy for VMs
51
+ function calculateNodeAssignments(vmNames, nodes, distribution) {
52
+ let nodeAssignments = [];
53
+ if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
54
+ // Distribute VMs across all available nodes
55
+ vmNames.forEach((vmName, index) => {
56
+ const nodeId = nodes[index % nodes.length];
57
+ nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
58
+ });
59
+ }
60
+ else if (distribution && distribution !== 'auto') {
61
+ // Parse specific node assignments like "1,2,3" or "node1,node2,node3"
62
+ const specifiedNodes = distribution.split(',').map((n) => {
63
+ const trimmed = n.trim();
64
+ return trimmed.startsWith('node') ? parseInt(trimmed.replace('node', '')) : parseInt(trimmed);
65
+ }).filter((n) => !isNaN(n));
66
+ if (specifiedNodes.length > 0) {
67
+ vmNames.forEach((vmName, index) => {
68
+ const nodeId = specifiedNodes[index % specifiedNodes.length];
69
+ nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
70
+ });
71
+ }
72
+ else {
73
+ nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
74
+ }
75
+ }
76
+ else {
77
+ // Auto-placement (no kvmHostId specified)
78
+ nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
79
+ }
80
+ return nodeAssignments;
81
+ }
82
+ exports.calculateNodeAssignments = calculateNodeAssignments;