@yuntools/ali-ecs
Version:
阿里云 ECS 模块封装,支持 ESM,CJS 导入,提供 TypeScript 类型定义
202 lines • 6.95 kB
JavaScript
import assert from 'node:assert';
import { DescribeInstancesRequest } from '@alicloud/ecs20140526';
import { Config as ApiConfig } from '@alicloud/openapi-client';
import { _Client } from './client.js';
import { Action, EcsStatusKey, EcsInfoKey, } from './types.js';
/**
* 阿里云 ECS 服务接口
* 最多支持 100 个实例
*/
export class EcsClient {
id;
secret;
endpoint;
client;
debug = false;
nextToken = '';
/** ip -> instanceId */
nodeIp2IdCache = new Map();
/** instanceId -> node */
id2NodeCache = new Map();
cacheTime;
cacheTTLSec = 30;
constructor(id, secret, endpoint = 'ecs-cn-hangzhou.aliyuncs.com') {
this.id = id;
this.secret = secret;
this.endpoint = endpoint;
this.client = this.createClient(id, secret);
}
/** 根据公网 IP 数组获取 Ecs 实例状态信息 */
async getNodeStatusByIps(ips, regionId = 'cn-hangzhou') {
const ret = new Map();
const nodes = await this.getInstancesByIps(ips, regionId);
nodes.forEach((row, ip) => {
if (typeof row === 'undefined') {
return;
}
const info = {};
Object.values(EcsStatusKey).forEach((key) => {
const value = row[key];
if (typeof value === 'undefined') {
return;
}
Object.defineProperty(info, key, {
enumerable: true,
value,
});
});
assert(Object.keys(info).length, 'info is empty');
ret.set(ip, info);
});
return ret;
}
/** 根据公网 IP 数组获取 Ecs 实例信息 */
async getNodeInfoByIps(ips, regionId = 'cn-hangzhou') {
const ret = new Map();
const nodes = await this.getInstancesByIps(ips, regionId);
nodes.forEach((row, ip) => {
if (typeof row === 'undefined') {
return;
}
const info = {};
Object.values(EcsInfoKey).forEach((key) => {
const value = row[key];
if (typeof value === 'undefined') {
return;
}
Object.defineProperty(info, key, {
enumerable: true,
value,
});
});
assert(Object.keys(info).length, 'info is empty');
ret.set(ip, info);
});
return ret;
}
/** 根据公网 IP 获取 Ecs 实例 ID */
async getInstanceIdByIp(ip) {
assert(ip, 'ip is required');
this.cleanCache();
const nodeId = this.nodeIp2IdCache.get(ip);
if (nodeId) {
this.debug && console.info(`getInstanceIdByIp from cache: ${ip} -> ${nodeId}`);
return nodeId;
}
const inst = await this.getInstanceByIp(ip);
return inst?.instanceId;
}
/** 根据公网 IP 数组获取 Ecs 实例信息 */
async getInstancesByIps(ips, regionId = 'cn-hangzhou') {
assert(Array.isArray(ips), 'ips must be an array');
this.cleanCache();
const ret = new Map();
for await (const ip of ips) {
assert(ip, 'ip is required');
assert(typeof ip === 'string', 'ip must be a string');
const inst = await this.getInstanceByIp(ip, regionId);
if (!inst) {
continue;
}
ret.set(ip, inst);
}
return ret;
}
/** 根据公网 IP 获取 Ecs 实例信息 */
async getInstanceByIp(ip, regionId = 'cn-hangzhou') {
assert(typeof ip === 'string', 'ip must be a string');
this.cleanCache();
const node = this._getInstanceByIpFromCache(ip.trim());
if (node) {
this.debug && console.log(`getInstanceByIp from cache: ${ip}`);
return node;
}
const opts = {
action: Action.DescribeInstances,
nextToken: this.nextToken,
regionId,
publicIpAddresses: [ip],
eipAddresses: [ip],
// pageNumber: 0,
pageSize: 100,
totalCount: 100,
};
const req = new DescribeInstancesRequest(opts);
this.debug && console.info({ req });
const resp = await this.client.describeInstances(req);
/* c8 ignore next 3 */
if (resp.body.nextToken) {
this.nextToken = resp.body.nextToken;
}
const insts = resp.body.instances?.instance;
assert(insts && Array.isArray(insts), 'insts is empty');
this.debug && console.log(`${ip} found ${insts.length}`);
this.updateInstancedCache(insts);
this.debug && console.info({ insts });
for (const inst of insts) {
const ips = inst.publicIpAddress?.ipAddress;
if (ips?.includes(ip)) {
return inst;
}
}
}
cleanCache(force = false) {
const now = Date.now();
if (force) {
this._cleanCache();
return;
}
const { cacheTime, cacheTTLSec } = this;
assert(typeof cacheTime === 'number' || typeof cacheTime === 'undefined');
assert(typeof cacheTTLSec === 'number', 'cacheTTLSec must be a number');
if (cacheTime && ((now - cacheTime) > cacheTTLSec * 1000)) {
console.log('cache expired');
this._cleanCache();
}
// void else
}
updateInstancedCache(instances) {
this.saveNodesToCache(instances);
this.cacheTime = Date.now();
}
_cleanCache() {
this.nodeIp2IdCache.clear();
this.id2NodeCache.clear();
this.cacheTime = 0;
}
_getInstanceByIpFromCache(ip) {
if (!ip.length) {
console.warn('_getInstanceByIpFromCache: ip is empty');
return;
}
const id = this.nodeIp2IdCache.get(ip);
if (!id) {
return;
}
const node = this.id2NodeCache.get(id);
return node;
}
createClient(accessKeyId, accessKeySecret) {
const config = new ApiConfig({ accessKeyId, accessKeySecret });
config.endpoint = this.endpoint;
const client = new _Client(config);
this.debug && console.info({ client });
return client;
}
saveNodesToCache(nodes) {
nodes.forEach((node) => {
const { instanceId } = node;
if (!instanceId) {
return;
}
if (node.eipAddress?.ipAddress) {
this.nodeIp2IdCache.set(node.eipAddress.ipAddress, instanceId);
}
node.publicIpAddress?.ipAddress?.forEach((ip) => {
ip && this.nodeIp2IdCache.set(ip, instanceId);
});
this.id2NodeCache.set(instanceId, node);
});
}
}
//# sourceMappingURL=ecs.js.map