UNPKG

@ddegtyarev/aws-tools

Version:

This project contains AWS API integration tools for use in Vertex AI SDK.

460 lines (459 loc) 20.4 kB
// src/tools/awsDescribeInstances.ts import { EC2Client, DescribeInstancesCommand, DescribeVolumesCommand } from '@aws-sdk/client-ec2'; import { validateParameters } from '../utils/validation.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as zlib from 'zlib'; import * as https from 'https'; // Pricing data cache let pricingDataCache = null; let pricingDataLastFetch = 0; const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds async function downloadAndCachePricingData(logger) { const cacheDir = path.join(os.tmpdir(), 'aws-tools-cache'); const cacheFile = path.join(cacheDir, 'ec2_pricing.json'); // Create cache directory if it doesn't exist if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } // Check if cache is still valid if (fs.existsSync(cacheFile)) { const stats = fs.statSync(cacheFile); const age = Date.now() - stats.mtime.getTime(); if (age < CACHE_DURATION) { try { const cachedData = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); logger?.debug('Using cached pricing data'); return cachedData; } catch (error) { logger?.warn('Failed to read cached pricing data, will download fresh copy'); } } } // Download fresh pricing data logger?.debug('Downloading fresh pricing data from S3'); const url = 'https://cloudfix-public-aws-pricing.s3.us-east-1.amazonaws.com/pricing/ec2_pricing.json.gz'; return new Promise((resolve, reject) => { https.get(url, (response) => { if (response.statusCode !== 200) { reject(new Error(`Failed to download pricing data: ${response.statusCode}`)); return; } const chunks = []; response.on('data', (chunk) => chunks.push(chunk)); response.on('end', () => { try { const compressedData = Buffer.concat(chunks); const decompressedData = zlib.gunzipSync(compressedData); const pricingData = JSON.parse(decompressedData.toString()); // Cache the data fs.writeFileSync(cacheFile, JSON.stringify(pricingData)); logger?.debug('Pricing data cached successfully'); resolve(pricingData); } catch (error) { reject(new Error(`Failed to process pricing data: ${error}`)); } }); }).on('error', (error) => { reject(new Error(`Failed to download pricing data: ${error.message}`)); }); }); } async function getPricingData(logger) { if (pricingDataCache && (Date.now() - pricingDataLastFetch) < CACHE_DURATION) { return pricingDataCache; } try { pricingDataCache = await downloadAndCachePricingData(logger); pricingDataLastFetch = Date.now(); return pricingDataCache; } catch (error) { logger?.warn('Failed to fetch pricing data, continuing without cost information:', error); return null; } } function getOperationCode(platform) { if (!platform) return ''; const platformLower = platform.toLowerCase(); if (platformLower.includes('windows')) { if (platformLower.includes('sql server enterprise')) return '0102'; if (platformLower.includes('sql server standard')) return '0006'; if (platformLower.includes('sql server web')) return '0202'; if (platformLower.includes('byol')) return '0800'; return '0002'; } else if (platformLower.includes('red hat')) { if (platformLower.includes('sql server enterprise')) return '0110'; if (platformLower.includes('sql server standard')) return '0014'; if (platformLower.includes('sql server web')) return '0210'; if (platformLower.includes('byol')) return '00g0'; return '0010'; } else if (platformLower.includes('suse')) { return '000g'; } else if (platformLower.includes('linux')) { if (platformLower.includes('sql server enterprise')) return '0100'; if (platformLower.includes('sql server standard')) return '0004'; if (platformLower.includes('sql server web')) return '0200'; return ''; } return ''; } function calculateInstanceCost(instanceType, platform, region, tenancy, pricingData) { try { // Parse instance type (e.g., "m5.large" -> family: "m5", size: "large") const match = instanceType.match(/^([a-z0-9]+)\.(.+)$/); if (!match) return null; const [, family, size] = match; const operationCode = getOperationCode(platform); const tenancyType = tenancy === 'dedicated' ? 'Dedicated' : 'Shared'; // Find pricing in the data structure const familyData = pricingData[family]; if (!familyData || !familyData.sizes || !familyData.sizes[size]) { return null; } const sizeData = familyData.sizes[size]; if (!sizeData.operations || !sizeData.operations[operationCode]) { return null; } const regionPricing = sizeData.operations[operationCode][region]; if (!regionPricing) return null; // Parse pricing string (comma-separated values) const prices = regionPricing.split(',').map((p) => parseFloat(p) || 0); // Get OnDemand price (position 0 for Shared, position 7 for Dedicated) const onDemandPriceIndex = tenancyType === 'Dedicated' ? 7 : 0; const onDemandHourlyCost = prices[onDemandPriceIndex]; // Get 3-year no-upfront Compute Savings Plan price (position 4 for Shared, position 11 for Dedicated) const savingsPlanPriceIndex = tenancyType === 'Dedicated' ? 11 : 4; const savingsPlanHourlyCost = prices[savingsPlanPriceIndex]; if (onDemandHourlyCost <= 0) return null; // Extract specifications const specifications = { vCPU: sizeData['vCPU, cores'] || 0, memory: sizeData['Memory, GB'] || 0, networkPerformance: sizeData['Network Performance, Mbps'] || 0, dedicatedEbsThroughput: sizeData['Dedicated EBS Throughput, Mbps'] || 0, ...(sizeData['GPU, cores'] > 0 && { gpu: sizeData['GPU, cores'] }), ...(sizeData['GPU Memory, GB'] > 0 && { gpuMemory: sizeData['GPU Memory, GB'] }) }; // Extract pricing details const pricingDetails = { family, size, operationCode, tenancyType, currentGeneration: familyData['Current Generation'] === 'Yes', instanceFamily: familyData['Instance Family'] || '', physicalProcessor: familyData['Physical Processor'] || '', clockSpeed: familyData['Clock Speed, GHz'] || 0, processorFeatures: familyData['Processor Features'] || '' }; return { onDemandCost: { hourlyCost: onDemandHourlyCost, monthlyCost: onDemandHourlyCost * 730 // 730 hours per month (average) }, savingsPlanCost: { hourlyCost: savingsPlanHourlyCost || 0, monthlyCost: (savingsPlanHourlyCost || 0) * 730 // 730 hours per month (average) }, specifications, pricingDetails }; } catch (error) { return null; } } async function fetchVolumeData(ec2Client, instanceIds, logger) { try { // Get all volume IDs attached to the instances const volumeCommand = new DescribeVolumesCommand({ Filters: [ { Name: 'attachment.instance-id', Values: instanceIds, }, ], }); const volumeData = await ec2Client.send(volumeCommand); logger?.debug('Volume data fetched:', JSON.stringify(volumeData, null, 2)); // Group volumes by instance ID const volumesByInstance = {}; volumeData.Volumes?.forEach(volume => { volume.Attachments?.forEach(attachment => { if (attachment.InstanceId) { if (!volumesByInstance[attachment.InstanceId]) { volumesByInstance[attachment.InstanceId] = []; } volumesByInstance[attachment.InstanceId].push({ volumeId: volume.VolumeId, size: volume.Size, volumeType: volume.VolumeType, iops: volume.Iops, encrypted: volume.Encrypted, }); } }); }); return volumesByInstance; } catch (error) { logger?.warn('Failed to fetch volume data, continuing without volume information:', error); return {}; } } function generateInstanceSummary(instances) { if (!instances || instances.length === 0) { return 'No EC2 instances found.'; } const instanceLines = instances.map(instance => { const instanceName = instance.instanceName || 'unnamed'; const state = instance.state || 'unknown'; const instanceType = instance.instanceType || 'unknown'; const platform = instance.platform || 'unknown'; const uptimeHours = instance.uptimeHours || 0; // Format uptime - use days if more than 48 hours let uptimeInfo = ''; if (uptimeHours > 48) { const days = Math.floor(uptimeHours / 24); uptimeInfo = `uptime ${days}d`; } else { uptimeInfo = `uptime ${uptimeHours}h`; } // Format cost information let costInfo = ''; if (instance.cost) { const onDemand = instance.cost.onDemandCost; const savingsPlan = instance.cost.savingsPlanCost; costInfo = `, ~$${onDemand.hourlyCost.toFixed(4)}/hr ($${onDemand.monthlyCost.toFixed(2)}/mo) OnDemand`; if (savingsPlan.hourlyCost > 0) { costInfo += `, ~$${savingsPlan.hourlyCost.toFixed(4)}/hr ($${savingsPlan.monthlyCost.toFixed(2)}/mo) 3yr Savings Plan`; } } // Format volumes concisely for cost analysis let volumeInfo = ''; if (instance.volumes && instance.volumes.length > 0) { const volumeSummary = instance.volumes .map((vol) => `${vol.size}GB ${vol.volumeType}`) .join('+'); volumeInfo = `, ${instance.volumes.length}×${volumeSummary} volume${instance.volumes.length > 1 ? 's' : ''}`; } // Format all tags information let tagInfo = ''; if (instance.tags && Object.keys(instance.tags).length > 0) { const tagPairs = Object.entries(instance.tags).map(([key, value]) => `${key}=${value}`); tagInfo = `, tags: ${tagPairs.join(', ')}`; } return `"${instanceName}" (${state}): ${instanceType} ${platform}, ${uptimeInfo}${costInfo}${volumeInfo}${tagInfo}`; }); return instanceLines.join('\n'); } export const awsDescribeInstances = { name: 'awsDescribeInstances', description: 'Get detailed information about EC2 instances including their configuration, state, and pricing.', inputSchema: { type: 'object', properties: { instanceIds: { type: 'array', items: { type: 'string' }, description: 'Specific instance IDs to describe' }, filters: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, values: { type: 'array', items: { type: 'string' } }, }, required: ['name', 'values'], }, description: 'Filters to apply to the describe operation', }, maxResults: { type: 'number', description: 'Maximum number of results to return (default: 1000)' }, }, }, outputSchema: { type: 'object', properties: { summary: { type: 'string', description: 'Text summary of the EC2 instances' }, datapoints: { type: 'array', items: { type: 'object', properties: { instanceId: { type: 'string' }, instanceName: { type: 'string' }, instanceType: { type: 'string' }, platform: { type: 'string' }, tenancy: { type: 'string' }, region: { type: 'string' }, uptimeHours: { type: 'number' }, state: { type: 'string' }, tags: { type: 'object' }, cost: { type: 'object', properties: { onDemandCost: { type: 'object', properties: { hourlyCost: { type: 'number' }, monthlyCost: { type: 'number' }, }, }, savingsPlanCost: { type: 'object', properties: { hourlyCost: { type: 'number' }, monthlyCost: { type: 'number' }, }, }, specifications: { type: 'object', properties: { vCPU: { type: 'number' }, memory: { type: 'number' }, networkPerformance: { type: 'number' }, dedicatedEbsThroughput: { type: 'number' }, gpu: { type: 'number' }, gpuMemory: { type: 'number' }, }, }, pricingDetails: { type: 'object', properties: { family: { type: 'string' }, size: { type: 'string' }, operationCode: { type: 'string' }, tenancyType: { type: 'string' }, currentGeneration: { type: 'boolean' }, instanceFamily: { type: 'string' }, physicalProcessor: { type: 'string' }, clockSpeed: { type: 'number' }, processorFeatures: { type: 'string' }, }, }, }, }, volumes: { type: 'array', items: { type: 'object', properties: { volumeId: { type: 'string' }, size: { type: 'number' }, volumeType: { type: 'string' }, iops: { type: 'number' }, encrypted: { type: 'boolean' }, }, }, }, }, }, }, }, }, configSchema: { type: 'object', properties: { credentials: { type: 'object', properties: { accessKeyId: { type: 'string' }, secretAccessKey: { type: 'string' }, sessionToken: { type: 'string' }, }, required: ['accessKeyId', 'secretAccessKey'], }, region: { type: 'string', description: 'AWS region where instances are located (e.g., "us-east-1")' }, logger: { type: 'object' }, }, required: ['credentials', 'region'], }, defaultConfig: {}, async invoke(input, config) { const { logger } = config; // Validate input and config against schemas validateParameters(input, this.inputSchema, config, this.configSchema, logger); const { instanceIds, filters, maxResults } = input; const { region } = config; logger?.debug('awsDescribeInstances input:', input); const ec2Client = new EC2Client({ region, credentials: config.credentials }); const command = new DescribeInstancesCommand({ InstanceIds: instanceIds, Filters: filters, MaxResults: maxResults, }); try { const data = await ec2Client.send(command); logger?.debug('awsDescribeInstances raw data:', JSON.stringify(data, null, 2)); // Extract basic instance information const instances = data.Reservations?.flatMap(reservation => reservation.Instances?.map(instance => ({ instanceId: instance.InstanceId, instanceName: instance.Tags?.find(tag => tag.Key === 'Name')?.Value || 'unnamed', instanceType: instance.InstanceType, platform: instance.PlatformDetails, tenancy: instance.Placement?.Tenancy, region: instance.Placement?.AvailabilityZone?.slice(0, -1), uptimeHours: instance.LaunchTime ? Math.floor((new Date().getTime() - instance.LaunchTime.getTime()) / 3600000) : 0, state: instance.State?.Name, tags: instance.Tags?.filter(tag => tag.Key !== 'Name').reduce((acc, tag) => { if (tag.Key && tag.Value) { acc[tag.Key] = tag.Value; } return acc; }, {}) || {}, }))) || []; // Fetch volume data for all instances const instanceIds = instances.map(inst => inst?.instanceId).filter(Boolean); const volumesByInstance = instanceIds.length > 0 ? await fetchVolumeData(ec2Client, instanceIds, logger) : {}; // Get pricing data const pricingData = await getPricingData(logger); // Merge volume data and add cost information const enrichedInstances = instances.map(instance => { if (!instance) return null; const volumes = instance.instanceId ? (volumesByInstance[instance.instanceId] || []) : []; const cost = pricingData ? calculateInstanceCost(instance.instanceType || '', instance.platform || '', instance.region || '', instance.tenancy || '', pricingData) : null; return { ...instance, volumes, cost, }; }).filter(Boolean); const summary = generateInstanceSummary(enrichedInstances); const output = { summary, datapoints: enrichedInstances, }; logger?.debug('awsDescribeInstances summary:\n', output.summary); logger?.debug('awsDescribeInstances datapoints:', output.datapoints); return output; } catch (error) { logger?.error('Error describing EC2 instances:', error); throw error; } }, };