vme-mcp-server 0.1.6 → 0.1.8

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.
@@ -13,19 +13,32 @@ exports.api = axios_1.default.create({
13
13
  "Content-Type": "application/json"
14
14
  }
15
15
  });
16
- // Helper function to get available cluster nodes
16
+ // Helper function to get available cluster nodes (hypervisor hosts, not VMs)
17
17
  async function getClusterNodes() {
18
18
  try {
19
+ // Get actual cluster hypervisor nodes from /clusters endpoint
19
20
  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);
21
+ let allNodes = [];
22
+ if (clusters.data.clusters) {
23
+ for (const cluster of clusters.data.clusters) {
24
+ if (cluster.servers && Array.isArray(cluster.servers)) {
25
+ const clusterNodes = cluster.servers.map((server) => server.id).filter((id) => id);
26
+ allNodes.push(...clusterNodes);
27
+ }
28
+ }
29
+ }
30
+ // Remove duplicates and sort
31
+ const uniqueNodes = [...new Set(allNodes)].sort((a, b) => a - b);
32
+ if (uniqueNodes.length > 0) {
33
+ console.log(`Found ${uniqueNodes.length} cluster hypervisor nodes: ${uniqueNodes.join(', ')}`);
34
+ return uniqueNodes;
23
35
  }
24
36
  }
25
37
  catch (error) {
26
38
  console.error('Error getting cluster nodes:', error);
27
39
  }
28
- // Fallback to known nodes if API fails
40
+ // Fallback to known hypervisor nodes if API fails
41
+ console.warn('Using fallback hypervisor nodes [1, 2, 3] - could not detect cluster hosts');
29
42
  return [1, 2, 3];
30
43
  }
31
44
  exports.getClusterNodes = getClusterNodes;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capabilityCache = exports.SmartCapabilityCache = void 0;
4
+ const api_utils_js_1 = require("./api-utils.js");
5
+ // Smart capability cache with field-level TTL
6
+ class SmartCapabilityCache {
7
+ constructor() {
8
+ this.cache = new Map();
9
+ // TTL configuration based on VME resource change patterns
10
+ this.ttlConfig = {
11
+ zones: 3600, // Zones rarely change (1 hour)
12
+ clusters: 1800, // Cluster topology changes occasionally (30 min)
13
+ service_plans: 1800, // Plans change occasionally (30 min)
14
+ instance_types: 7200, // Instance types very stable (2 hours)
15
+ virtual_images: 900, // Images updated more frequently (15 min)
16
+ cluster_status: 300, // Node status changes often (5 min)
17
+ resource_pools: 1800, // Pool config changes occasionally (30 min)
18
+ networks: 3600, // Network config rarely changes (1 hour)
19
+ groups: 3600, // Groups rarely change (1 hour)
20
+ };
21
+ // Default TTL for unknown fields
22
+ this.defaultTTL = 600; // 10 minutes
23
+ }
24
+ /**
25
+ * Get data for a specific field, using cache if fresh or fetching if stale
26
+ */
27
+ async getField(fieldName, forceRefresh = false) {
28
+ const cached = this.cache.get(fieldName);
29
+ const now = Math.floor(Date.now() / 1000);
30
+ // Check if cache is fresh and not forcing refresh
31
+ if (!forceRefresh && cached && this.isFresh(cached, now)) {
32
+ return cached.data;
33
+ }
34
+ // Fetch fresh data from VME API
35
+ const endpoint = this.getEndpointForField(fieldName);
36
+ try {
37
+ const response = await api_utils_js_1.api.get(endpoint);
38
+ // Store in cache with appropriate TTL
39
+ const cacheEntry = {
40
+ data: response.data,
41
+ cached_at: now,
42
+ ttl_seconds: this.ttlConfig[fieldName] || this.defaultTTL,
43
+ endpoint: endpoint,
44
+ field_name: fieldName
45
+ };
46
+ this.cache.set(fieldName, cacheEntry);
47
+ return response.data;
48
+ }
49
+ catch (error) {
50
+ // If API fails and we have stale cache, return it with warning
51
+ if (cached) {
52
+ console.warn(`VME API failed for ${fieldName}, returning stale cache data`);
53
+ return cached.data;
54
+ }
55
+ // No cache and API failed - propagate error
56
+ throw new Error(`Failed to fetch ${fieldName}: ${error.message}`);
57
+ }
58
+ }
59
+ /**
60
+ * Get cache status for a specific field or all fields
61
+ */
62
+ getCacheStatus(fieldName) {
63
+ const now = Math.floor(Date.now() / 1000);
64
+ const statuses = [];
65
+ if (fieldName) {
66
+ const cached = this.cache.get(fieldName);
67
+ if (cached) {
68
+ statuses.push(this.buildCacheStatus(cached, now));
69
+ }
70
+ }
71
+ else {
72
+ // Return status for all cached fields
73
+ for (const cached of this.cache.values()) {
74
+ statuses.push(this.buildCacheStatus(cached, now));
75
+ }
76
+ }
77
+ return statuses;
78
+ }
79
+ /**
80
+ * Invalidate cache for specific field or all fields
81
+ */
82
+ invalidate(fieldName) {
83
+ if (fieldName) {
84
+ this.cache.delete(fieldName);
85
+ }
86
+ else {
87
+ this.cache.clear();
88
+ }
89
+ }
90
+ /**
91
+ * Get cache hit rate statistics
92
+ */
93
+ getStatistics() {
94
+ // This would need hit/miss tracking - placeholder for now
95
+ return { hits: 0, misses: 0, hitRate: 0 };
96
+ }
97
+ // Private helper methods
98
+ isFresh(cached, now) {
99
+ return (now - cached.cached_at) < cached.ttl_seconds;
100
+ }
101
+ buildCacheStatus(cached, now) {
102
+ const age = now - cached.cached_at;
103
+ const expiresIn = cached.ttl_seconds - age;
104
+ return {
105
+ field_name: cached.field_name,
106
+ cached_at: cached.cached_at,
107
+ ttl_seconds: cached.ttl_seconds,
108
+ age_seconds: age,
109
+ is_fresh: this.isFresh(cached, now),
110
+ expires_in_seconds: Math.max(0, expiresIn),
111
+ endpoint: cached.endpoint
112
+ };
113
+ }
114
+ getEndpointForField(fieldName) {
115
+ // Map field names to VME API endpoints
116
+ const endpointMap = {
117
+ zones: "/zones",
118
+ clusters: "/clusters",
119
+ service_plans: "/service-plans",
120
+ instance_types: "/instance-types",
121
+ virtual_images: "/virtual-images",
122
+ resource_pools: "/resource-pools",
123
+ networks: "/networks",
124
+ groups: "/groups"
125
+ };
126
+ return endpointMap[fieldName] || `/${fieldName}`;
127
+ }
128
+ }
129
+ exports.SmartCapabilityCache = SmartCapabilityCache;
130
+ // Global cache instance
131
+ exports.capabilityCache = new SmartCapabilityCache();
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capabilityDiscovery = exports.CapabilityDiscoveryEngine = void 0;
4
+ const capability_cache_js_1 = require("./capability-cache.js");
5
+ // Capability discovery engine
6
+ class CapabilityDiscoveryEngine {
7
+ /**
8
+ * Discover capabilities for specified domains
9
+ */
10
+ async discoverCapabilities(domains = ["all"], forceRefresh = false) {
11
+ const result = {
12
+ discovered_at: new Date().toISOString(),
13
+ cache_status: []
14
+ };
15
+ // Determine which domains to discover
16
+ const shouldDiscover = (domain) => domains.includes("all") || domains.includes(domain);
17
+ try {
18
+ // Discover capabilities in parallel for efficiency
19
+ const discoveries = await Promise.allSettled([
20
+ shouldDiscover("compute") ? this.discoverComputeCapabilities(forceRefresh) : Promise.resolve(null),
21
+ shouldDiscover("networking") ? this.discoverNetworkingCapabilities(forceRefresh) : Promise.resolve(null),
22
+ shouldDiscover("storage") ? this.discoverStorageCapabilities(forceRefresh) : Promise.resolve(null),
23
+ shouldDiscover("platform") ? this.discoverPlatformCapabilities(forceRefresh) : Promise.resolve(null),
24
+ ]);
25
+ // Process results
26
+ if (discoveries[0]?.status === "fulfilled" && discoveries[0].value) {
27
+ result.compute = discoveries[0].value;
28
+ }
29
+ if (discoveries[1]?.status === "fulfilled" && discoveries[1].value) {
30
+ result.networking = discoveries[1].value;
31
+ }
32
+ if (discoveries[2]?.status === "fulfilled" && discoveries[2].value) {
33
+ result.storage = discoveries[2].value;
34
+ }
35
+ if (discoveries[3]?.status === "fulfilled" && discoveries[3].value) {
36
+ result.platform = discoveries[3].value;
37
+ }
38
+ // Add cache status summary
39
+ result.cache_status = this.getCacheStatusSummary();
40
+ return result;
41
+ }
42
+ catch (error) {
43
+ throw new Error(`Capability discovery failed: ${error.message}`);
44
+ }
45
+ }
46
+ /**
47
+ * Check if a specific capability is available
48
+ */
49
+ async checkCapability(question, capabilityType) {
50
+ // Simple natural language processing for capability questions
51
+ const questionLower = question.toLowerCase();
52
+ try {
53
+ // GPU support check
54
+ if (questionLower.includes("gpu") || capabilityType === "gpu_support") {
55
+ const compute = await this.discoverComputeCapabilities();
56
+ return {
57
+ answer: compute.gpu_support,
58
+ details: compute.gpu_support
59
+ ? "GPU support is available in this VME environment"
60
+ : "No GPU support detected in cluster nodes",
61
+ confidence: 0.95
62
+ };
63
+ }
64
+ // Hypervisor checks
65
+ if (questionLower.includes("vmware") || questionLower.includes("kvm") || questionLower.includes("hypervisor")) {
66
+ const compute = await this.discoverComputeCapabilities();
67
+ const hypervisor = questionLower.includes("vmware") ? "VMware" :
68
+ questionLower.includes("kvm") ? "KVM" : null;
69
+ if (hypervisor) {
70
+ const available = compute.hypervisors.includes(hypervisor);
71
+ return {
72
+ answer: available,
73
+ details: available
74
+ ? `${hypervisor} hypervisor is available`
75
+ : `${hypervisor} hypervisor not found. Available: ${compute.hypervisors.join(", ")}`,
76
+ confidence: 0.9
77
+ };
78
+ }
79
+ }
80
+ // CPU/Memory limit checks
81
+ if (questionLower.match(/\b(\d+)\s*(cpu|core|vcpu)/)) {
82
+ const compute = await this.discoverComputeCapabilities();
83
+ const requestedCPU = parseInt(questionLower.match(/\b(\d+)\s*(cpu|core|vcpu)/)?.[1] || "0");
84
+ const canSupport = requestedCPU <= compute.max_cpu_per_vm;
85
+ return {
86
+ answer: canSupport,
87
+ details: canSupport
88
+ ? `${requestedCPU} CPU configuration is supported (max: ${compute.max_cpu_per_vm})`
89
+ : `${requestedCPU} CPU exceeds maximum of ${compute.max_cpu_per_vm} per VM`,
90
+ confidence: 0.85
91
+ };
92
+ }
93
+ // Generic fallback
94
+ return {
95
+ answer: false,
96
+ details: "Unable to determine capability from question. Try being more specific.",
97
+ confidence: 0.1
98
+ };
99
+ }
100
+ catch (error) {
101
+ return {
102
+ answer: false,
103
+ details: `Error checking capability: ${error.message}`,
104
+ confidence: 0.0
105
+ };
106
+ }
107
+ }
108
+ // Private discovery methods for each domain
109
+ async discoverComputeCapabilities(forceRefresh = false) {
110
+ const [clusters, servicePlans, instanceTypes, virtualImages] = await Promise.all([
111
+ capability_cache_js_1.capabilityCache.getField("clusters", forceRefresh),
112
+ capability_cache_js_1.capabilityCache.getField("service_plans", forceRefresh),
113
+ capability_cache_js_1.capabilityCache.getField("instance_types", forceRefresh),
114
+ capability_cache_js_1.capabilityCache.getField("virtual_images", forceRefresh),
115
+ ]);
116
+ // Derive hypervisors from cluster data
117
+ const hypervisors = new Set();
118
+ let maxCPU = 0;
119
+ let maxMemoryGB = 0;
120
+ let hasGPU = false;
121
+ if (clusters?.clusters) {
122
+ for (const cluster of clusters.clusters) {
123
+ if (cluster.servers) {
124
+ for (const server of cluster.servers) {
125
+ if (server.serverType) {
126
+ hypervisors.add(server.serverType);
127
+ }
128
+ if (server.maxCpu && server.maxCpu > maxCPU) {
129
+ maxCPU = server.maxCpu;
130
+ }
131
+ if (server.maxMemory && server.maxMemory > maxMemoryGB) {
132
+ maxMemoryGB = Math.floor(server.maxMemory / (1024 * 1024 * 1024)); // Convert to GB
133
+ }
134
+ if (server.gpuCount && server.gpuCount > 0) {
135
+ hasGPU = true;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ // Process service plans for limits
142
+ const plans = servicePlans?.servicePlans?.map((plan) => ({
143
+ id: plan.id,
144
+ name: plan.name,
145
+ max_cpu: plan.maxCpu,
146
+ max_memory: plan.maxMemory,
147
+ max_storage: plan.maxStorage
148
+ })) || [];
149
+ // Process instance types
150
+ const instances = instanceTypes?.instanceTypes?.map((type) => ({
151
+ id: type.id,
152
+ name: type.name,
153
+ code: type.code
154
+ })) || [];
155
+ // Process virtual images
156
+ const images = virtualImages?.virtualImages?.map((img) => ({
157
+ id: img.id,
158
+ name: img.name,
159
+ os_type: img.osType?.name || "Unknown",
160
+ category: img.osType?.category || "unknown",
161
+ version: img.osType?.osVersion
162
+ })) || [];
163
+ return {
164
+ hypervisors: Array.from(hypervisors),
165
+ instance_types: instances,
166
+ service_plans: plans,
167
+ max_cpu_per_vm: maxCPU,
168
+ max_memory_per_vm: `${maxMemoryGB}GB`,
169
+ gpu_support: hasGPU,
170
+ available_images: images.slice(0, 10) // Limit to prevent token bloat
171
+ };
172
+ }
173
+ async discoverNetworkingCapabilities(forceRefresh = false) {
174
+ const [zones, networks] = await Promise.all([
175
+ capability_cache_js_1.capabilityCache.getField("zones", forceRefresh),
176
+ capability_cache_js_1.capabilityCache.getField("networks", forceRefresh).catch(() => null), // Optional
177
+ ]);
178
+ const zoneList = zones?.zones?.map((zone) => ({
179
+ id: zone.id,
180
+ name: zone.name,
181
+ description: zone.description
182
+ })) || [];
183
+ return {
184
+ zones: zoneList,
185
+ network_types: ["VLAN"], // Default for VME
186
+ load_balancer_support: false, // Would need to check features
187
+ firewall_support: false, // Would need to check features
188
+ };
189
+ }
190
+ async discoverStorageCapabilities(forceRefresh = false) {
191
+ // Get real data from VME API
192
+ const servicePlans = await capability_cache_js_1.capabilityCache.getField("service_plans", forceRefresh);
193
+ // Derive storage capabilities from actual service plans and other data
194
+ const storageTypes = new Set();
195
+ let maxVolumeSize = "1TB"; // Default
196
+ if (servicePlans?.servicePlans) {
197
+ for (const plan of servicePlans.servicePlans) {
198
+ // Look for storage-related info in service plans
199
+ if (plan.maxStorage) {
200
+ const sizeGB = Math.floor(plan.maxStorage / (1024 * 1024 * 1024));
201
+ if (sizeGB > parseInt(maxVolumeSize)) {
202
+ maxVolumeSize = `${sizeGB}GB`;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ // Basic storage types - would need more VME API endpoints to get real storage info
208
+ storageTypes.add("Local"); // VME always has local storage
209
+ return {
210
+ storage_types: Array.from(storageTypes),
211
+ max_volume_size: maxVolumeSize,
212
+ snapshot_support: true, // VME typically supports snapshots
213
+ encryption_support: false, // Would need to check actual VME features
214
+ };
215
+ }
216
+ async discoverPlatformCapabilities(forceRefresh = false) {
217
+ const groups = await capability_cache_js_1.capabilityCache.getField("groups", forceRefresh);
218
+ const groupList = groups?.groups?.map((group) => ({
219
+ id: group.id,
220
+ name: group.name,
221
+ description: group.description
222
+ })) || [];
223
+ return {
224
+ api_version: "6.2.x", // Would get from API info endpoint
225
+ groups: groupList,
226
+ enabled_features: ["VM Provisioning", "Resource Management"], // Would derive from license/features
227
+ };
228
+ }
229
+ getCacheStatusSummary() {
230
+ const statuses = capability_cache_js_1.capabilityCache.getCacheStatus();
231
+ return statuses.map(status => ({
232
+ field: status.field_name,
233
+ fresh: status.is_fresh,
234
+ age_seconds: status.age_seconds
235
+ }));
236
+ }
237
+ }
238
+ exports.CapabilityDiscoveryEngine = CapabilityDiscoveryEngine;
239
+ // Global discovery engine instance
240
+ exports.capabilityDiscovery = new CapabilityDiscoveryEngine();
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nameResolver = exports.NameResolver = void 0;
4
+ const capability_discovery_js_1 = require("./capability-discovery.js");
5
+ // Generic name->ID resolver using capability discovery data
6
+ class NameResolver {
7
+ constructor() {
8
+ this.capabilitiesCache = null;
9
+ this.cacheTimestamp = 0;
10
+ this.CACHE_TTL = 300000; // 5 minutes
11
+ }
12
+ async getCapabilities() {
13
+ const now = Date.now();
14
+ if (!this.capabilitiesCache || (now - this.cacheTimestamp) > this.CACHE_TTL) {
15
+ this.capabilitiesCache = await capability_discovery_js_1.capabilityDiscovery.discoverCapabilities(['all']);
16
+ this.cacheTimestamp = now;
17
+ }
18
+ return this.capabilitiesCache;
19
+ }
20
+ async resolveNameToId(resourceType, name) {
21
+ if (!name)
22
+ return null;
23
+ const capabilities = await this.getCapabilities();
24
+ const nameLower = name.toLowerCase();
25
+ switch (resourceType) {
26
+ case 'virtualImage':
27
+ case 'image':
28
+ return this.resolveImageName(capabilities, nameLower);
29
+ case 'group':
30
+ case 'site':
31
+ return this.resolveGroupName(capabilities, nameLower);
32
+ case 'servicePlan':
33
+ case 'plan':
34
+ return this.resolvePlanName(capabilities, nameLower);
35
+ case 'zone':
36
+ case 'cloud':
37
+ return this.resolveZoneName(capabilities, nameLower);
38
+ case 'instanceType':
39
+ return this.resolveInstanceTypeName(capabilities, nameLower);
40
+ default:
41
+ return null;
42
+ }
43
+ }
44
+ resolveImageName(capabilities, nameLower) {
45
+ const images = capabilities.compute?.available_images || [];
46
+ // Try exact name match first
47
+ let found = images.find((img) => img.name?.toLowerCase() === nameLower);
48
+ if (found)
49
+ return found.id;
50
+ // Try partial name match
51
+ found = images.find((img) => img.name?.toLowerCase().includes(nameLower));
52
+ if (found)
53
+ return found.id;
54
+ // Try category + version matching (e.g., "ubuntu 20.04" -> ubuntu-2004)
55
+ for (const img of images) {
56
+ const imgName = img.name?.toLowerCase() || '';
57
+ const category = img.category?.toLowerCase() || '';
58
+ const version = img.version?.toString() || '';
59
+ // Match patterns like "ubuntu 20.04" to "ubuntu-2004"
60
+ if (nameLower.includes(category) && nameLower.includes(version)) {
61
+ return img.id;
62
+ }
63
+ // Match patterns like "ubuntu" to first ubuntu image
64
+ if (nameLower === category) {
65
+ return img.id;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ resolveGroupName(capabilities, nameLower) {
71
+ const groups = capabilities.platform?.groups || [];
72
+ // Try exact match
73
+ let found = groups.find((g) => g.name?.toLowerCase() === nameLower);
74
+ if (found)
75
+ return found.id;
76
+ // Try partial match
77
+ found = groups.find((g) => g.name?.toLowerCase().includes(nameLower));
78
+ if (found)
79
+ return found.id;
80
+ return null;
81
+ }
82
+ resolvePlanName(capabilities, nameLower) {
83
+ const plans = capabilities.compute?.service_plans || [];
84
+ // Try exact match
85
+ let found = plans.find((p) => p.name?.toLowerCase() === nameLower);
86
+ if (found)
87
+ return found.id;
88
+ // Try size-based matching
89
+ if (nameLower.includes('small') || nameLower.includes('1 cpu') || nameLower.includes('4gb')) {
90
+ found = plans.find((p) => p.name?.toLowerCase().includes('1 cpu') ||
91
+ p.name?.toLowerCase().includes('4gb') ||
92
+ p.max_cpu === 1);
93
+ if (found)
94
+ return found.id;
95
+ }
96
+ if (nameLower.includes('medium') || nameLower.includes('2 cpu') || nameLower.includes('8gb')) {
97
+ found = plans.find((p) => p.name?.toLowerCase().includes('2 cpu') ||
98
+ p.name?.toLowerCase().includes('8gb') ||
99
+ p.max_cpu === 2);
100
+ if (found)
101
+ return found.id;
102
+ }
103
+ if (nameLower.includes('large') || nameLower.includes('4 cpu') || nameLower.includes('16gb')) {
104
+ found = plans.find((p) => p.name?.toLowerCase().includes('4 cpu') ||
105
+ p.name?.toLowerCase().includes('16gb') ||
106
+ p.max_cpu === 4);
107
+ if (found)
108
+ return found.id;
109
+ }
110
+ // Try partial match
111
+ found = plans.find((p) => p.name?.toLowerCase().includes(nameLower));
112
+ if (found)
113
+ return found.id;
114
+ return null;
115
+ }
116
+ resolveZoneName(capabilities, nameLower) {
117
+ const zones = capabilities.networking?.zones || [];
118
+ // Try exact match
119
+ let found = zones.find((z) => z.name?.toLowerCase() === nameLower);
120
+ if (found)
121
+ return found.id;
122
+ // Try partial match
123
+ found = zones.find((z) => z.name?.toLowerCase().includes(nameLower));
124
+ if (found)
125
+ return found.id;
126
+ return null;
127
+ }
128
+ resolveInstanceTypeName(capabilities, nameLower) {
129
+ const instanceTypes = capabilities.compute?.instance_types || [];
130
+ // Try exact match
131
+ let found = instanceTypes.find((t) => t.name?.toLowerCase() === nameLower);
132
+ if (found)
133
+ return found.id;
134
+ // Try code match
135
+ found = instanceTypes.find((t) => t.code?.toLowerCase() === nameLower);
136
+ if (found)
137
+ return found.id;
138
+ // Try partial match
139
+ found = instanceTypes.find((t) => t.name?.toLowerCase().includes(nameLower));
140
+ if (found)
141
+ return found.id;
142
+ return null;
143
+ }
144
+ // Helper method to get available names for a resource type
145
+ async getAvailableNames(resourceType) {
146
+ const capabilities = await this.getCapabilities();
147
+ switch (resourceType) {
148
+ case 'virtualImage':
149
+ return capabilities.compute?.available_images?.map((img) => img.name) || [];
150
+ case 'group':
151
+ return capabilities.platform?.groups?.map((g) => g.name) || [];
152
+ case 'servicePlan':
153
+ return capabilities.compute?.service_plans?.map((p) => p.name) || [];
154
+ case 'zone':
155
+ return capabilities.networking?.zones?.map((z) => z.name) || [];
156
+ case 'instanceType':
157
+ return capabilities.compute?.instance_types?.map((t) => t.name) || [];
158
+ default:
159
+ return [];
160
+ }
161
+ }
162
+ // Helper to resolve multiple resources at once
163
+ async resolveMultiple(resources) {
164
+ const results = {};
165
+ for (const resource of resources) {
166
+ const key = `${resource.type}_${resource.name}`;
167
+ results[key] = await this.resolveNameToId(resource.type, resource.name);
168
+ }
169
+ return results;
170
+ }
171
+ }
172
+ exports.NameResolver = NameResolver;
173
+ // Global resolver instance
174
+ exports.nameResolver = new NameResolver();
@@ -3,10 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.calculateNodeAssignments = exports.parseVMNames = void 0;
4
4
  // Helper function to parse VM name patterns
5
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;
6
+ // Handle letter range patterns like "ws-a->ws-x" or "ws-a -> ws-x"
7
+ const letterRangeMatch = nameInput.match(/^(.+?)([a-z])\s*->\s*(.+?)([a-z])$/i);
8
+ if (letterRangeMatch) {
9
+ const [, prefix1, startLetter, prefix2, endLetter] = letterRangeMatch;
10
+ if (prefix1.trim() === prefix2.trim()) {
11
+ const basePrefix = prefix1.trim();
12
+ const startCode = startLetter.toLowerCase().charCodeAt(0);
13
+ const endCode = endLetter.toLowerCase().charCodeAt(0);
14
+ const names = [];
15
+ for (let i = startCode; i <= endCode; i++) {
16
+ const letter = String.fromCharCode(i);
17
+ names.push(`${basePrefix}${letter}`);
18
+ }
19
+ return names;
20
+ }
21
+ }
22
+ // Handle numeric range patterns like "web01->web03" or "web01 -> web03"
23
+ const numericRangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
24
+ if (numericRangeMatch) {
25
+ const [, prefix1, start, prefix2, end] = numericRangeMatch;
10
26
  if (prefix1.trim() === prefix2.trim()) {
11
27
  const basePrefix = prefix1.trim();
12
28
  const startNum = parseInt(start);
@@ -57,21 +73,13 @@ function calculateNodeAssignments(vmNames, nodes, distribution) {
57
73
  nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
58
74
  });
59
75
  }
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
- }
76
+ else if (distribution && !['auto', 'spread'].includes(distribution)) {
77
+ // Distribution is a node/hypervisor name - signal that it needs resolution
78
+ // Return assignments with the node name for the calling function to resolve
79
+ nodeAssignments = vmNames.map(vmName => ({
80
+ name: vmName,
81
+ nodeNameToResolve: distribution
82
+ }));
75
83
  }
76
84
  else {
77
85
  // Auto-placement (no kvmHostId specified)