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.
- package/dist/lib/api-utils.js +18 -5
- package/dist/lib/capability-cache.js +131 -0
- package/dist/lib/capability-discovery.js +240 -0
- package/dist/lib/name-resolver.js +174 -0
- package/dist/lib/vm-parsing.js +27 -19
- package/dist/tools/check-capability.js +84 -0
- package/dist/tools/create-vm.js +95 -23
- package/dist/tools/discover-capabilities.js +79 -0
- package/dist/tools/get-cache-status.js +123 -0
- package/dist/tools/get-resources.js +1 -1
- package/dist/tools/index.js +37 -10
- package/dist/tools/query-resources.js +371 -0
- package/package.json +4 -4
package/dist/lib/api-utils.js
CHANGED
@@ -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
|
-
|
21
|
-
if (
|
22
|
-
|
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();
|
package/dist/lib/vm-parsing.js
CHANGED
@@ -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 "
|
7
|
-
const
|
8
|
-
if (
|
9
|
-
const [, prefix1,
|
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 &&
|
61
|
-
//
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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)
|