aciApi.ts•11.4 kB
import * as https from 'https';
import * as crypto from 'crypto';
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
export interface ACIConfig {
apicUrl: string;
username: string;
password?: string;
certificateName?: string;
privateKey?: string;
validateCerts?: boolean;
timeout?: number;
}
export interface ACISession {
token: string;
refreshToken?: string;
expires: number;
}
export class ACIApiService {
private client: AxiosInstance;
private config: ACIConfig;
private session: ACISession | null = null;
constructor(config: ACIConfig) {
this.config = {
validateCerts: false,
timeout: 30000,
...config
};
// Create axios instance with custom config
this.client = axios.create({
baseURL: this.config.apicUrl,
timeout: this.config.timeout,
httpsAgent: new https.Agent({
rejectUnauthorized: this.config.validateCerts
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
// Add request interceptor for authentication
this.client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
if (!this.isAuthenticated() && !config.url?.includes('/api/aaaLogin')) {
await this.authenticate();
}
if (this.session?.token && config.headers) {
config.headers['Cookie'] = `APIC-Cookie=${this.session.token}`;
}
return config;
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 || error.response?.status === 403) {
this.session = null;
// Retry the request after re-authentication
if (!error.config.url?.includes('/api/aaaLogin')) {
await this.authenticate();
return this.client.request(error.config);
}
}
throw error;
}
);
}
private isAuthenticated(): boolean {
return this.session !== null && Date.now() < this.session.expires;
}
public async authenticate(): Promise<void> {
try {
if (this.config.privateKey && this.config.certificateName) {
await this.authenticateWithCertificate();
} else if (this.config.password) {
await this.authenticateWithPassword();
} else {
throw new Error('No authentication method configured');
}
} catch (error) {
console.error('Authentication failed:', error);
throw error;
}
}
private async authenticateWithPassword(): Promise<void> {
const loginData = {
aaaUser: {
attributes: {
name: this.config.username,
pwd: this.config.password
}
}
};
const response = await this.client.post('/api/aaaLogin.json', loginData);
const loginResult = response.data?.imdata?.[0]?.aaaLogin?.attributes;
if (!loginResult?.token) {
throw new Error('Authentication failed: No token received');
}
this.session = {
token: loginResult.token,
refreshToken: loginResult.refreshToken,
expires: Date.now() + (parseInt(loginResult.refreshTimeoutSeconds || '600') * 1000)
};
}
private async authenticateWithCertificate(): Promise<void> {
const payload = `GET/api/aaaRefresh.json`;
const signature = crypto.sign('sha256', Buffer.from(payload), this.config.privateKey!);
const encodedSignature = Buffer.from(signature).toString('base64');
const headers = {
'Cookie': `APIC-Request-Signature=${encodedSignature}; APIC-Certificate-Algorithm=v1.0; APIC-Certificate-Fingerprint=fingerprint; APIC-Certificate-DN=uni/userext/user-${this.config.username}/usercert-${this.config.certificateName}`
};
const response = await this.client.get('/api/aaaRefresh.json', { headers });
const refreshResult = response.data?.imdata?.[0]?.aaaRefresh?.attributes;
if (!refreshResult?.token) {
throw new Error('Certificate authentication failed');
}
this.session = {
token: refreshResult.token,
expires: Date.now() + (parseInt(refreshResult.refreshTimeoutSeconds || '600') * 1000)
};
}
async get(path: string, params?: Record<string, any>): Promise<any> {
const response = await this.client.get(path, { params });
return response.data;
}
async post(path: string, data: any): Promise<any> {
const response = await this.client.post(path, data);
return response.data;
}
async put(path: string, data: any): Promise<any> {
const response = await this.client.put(path, data);
return response.data;
}
async delete(path: string): Promise<any> {
const response = await this.client.delete(path);
return response.data;
}
// ACI-specific methods
async getTenants(): Promise<any> {
return this.get('/api/node/class/fvTenant.json');
}
async getApplicationProfiles(tenant?: string): Promise<any> {
const filter = tenant ? `?query-target-filter=wcard(fvAp.dn,"${tenant}")` : '';
return this.get(`/api/node/class/fvAp.json${filter}`);
}
async getEndpointGroups(tenant?: string, appProfile?: string): Promise<any> {
let filter = '';
if (tenant && appProfile) {
filter = `?query-target-filter=wcard(fvAEPg.dn,"${tenant}/ap-${appProfile}")`;
} else if (tenant) {
filter = `?query-target-filter=wcard(fvAEPg.dn,"${tenant}")`;
}
return this.get(`/api/node/class/fvAEPg.json${filter}`);
}
async getBridgeDomains(tenant?: string): Promise<any> {
const filter = tenant ? `?query-target-filter=wcard(fvBD.dn,"${tenant}")` : '';
return this.get(`/api/node/class/fvBD.json${filter}`);
}
async getVRFs(tenant?: string): Promise<any> {
const filter = tenant ? `?query-target-filter=wcard(fvCtx.dn,"${tenant}")` : '';
return this.get(`/api/node/class/fvCtx.json${filter}`);
}
async getContracts(tenant?: string): Promise<any> {
const filter = tenant ? `?query-target-filter=wcard(vzBrCP.dn,"${tenant}")` : '';
return this.get(`/api/node/class/vzBrCP.json${filter}`);
}
async getFilters(tenant?: string): Promise<any> {
const filter = tenant ? `?query-target-filter=wcard(vzFilter.dn,"${tenant}")` : '';
return this.get(`/api/node/class/vzFilter.json${filter}`);
}
async getPhysicalDomains(): Promise<any> {
return this.get('/api/node/class/physDomP.json');
}
async getVlanPools(): Promise<any> {
return this.get('/api/node/class/fvnsVlanInstP.json');
}
async getNodes(): Promise<any> {
return this.get('/api/node/class/fabricNode.json');
}
async getInterfaces(): Promise<any> {
return this.get('/api/node/class/l1PhysIf.json');
}
async getFabricHealth(): Promise<any> {
return this.get('/api/node/class/fabricHealthTotal.json');
}
async getFaults(): Promise<any> {
return this.get('/api/node/class/faultSummary.json');
}
async createTenant(name: string, description?: string): Promise<any> {
const tenantData = {
fvTenant: {
attributes: {
name: name,
descr: description || '',
dn: `uni/tn-${name}`
}
}
};
return this.post('/api/node/mo/uni.json', tenantData);
}
async createApplicationProfile(tenant: string, name: string, description?: string): Promise<any> {
const apData = {
fvAp: {
attributes: {
name: name,
descr: description || '',
dn: `uni/tn-${tenant}/ap-${name}`
}
}
};
return this.post(`/api/node/mo/uni/tn-${tenant}.json`, apData);
}
async createEndpointGroup(tenant: string, appProfile: string, name: string, bridgeDomain?: string): Promise<any> {
const epgData: any = {
fvAEPg: {
attributes: {
name: name,
dn: `uni/tn-${tenant}/ap-${appProfile}/epg-${name}`
}
}
};
if (bridgeDomain) {
epgData.fvAEPg.children = [{
fvRsBd: {
attributes: {
tnFvBDName: bridgeDomain
}
}
}];
}
return this.post(`/api/node/mo/uni/tn-${tenant}/ap-${appProfile}.json`, epgData);
}
async createBridgeDomain(tenant: string, name: string, vrf?: string): Promise<any> {
const bdData: any = {
fvBD: {
attributes: {
name: name,
dn: `uni/tn-${tenant}/BD-${name}`
}
}
};
if (vrf) {
bdData.fvBD.children = [{
fvRsCtx: {
attributes: {
tnFvCtxName: vrf
}
}
}];
}
return this.post(`/api/node/mo/uni/tn-${tenant}.json`, bdData);
}
async createVRF(tenant: string, name: string, description?: string): Promise<any> {
const vrfData = {
fvCtx: {
attributes: {
name: name,
descr: description || '',
dn: `uni/tn-${tenant}/ctx-${name}`
}
}
};
return this.post(`/api/node/mo/uni/tn-${tenant}.json`, vrfData);
}
async createContract(tenant: string, name: string, scope: string = 'context'): Promise<any> {
const contractData = {
vzBrCP: {
attributes: {
name: name,
scope: scope,
dn: `uni/tn-${tenant}/brc-${name}`
}
}
};
return this.post(`/api/node/mo/uni/tn-${tenant}.json`, contractData);
}
async createFilter(tenant: string, name: string): Promise<any> {
const filterData = {
vzFilter: {
attributes: {
name: name,
dn: `uni/tn-${tenant}/flt-${name}`
}
}
};
return this.post(`/api/node/mo/uni/tn-${tenant}.json`, filterData);
}
async deleteObject(dn: string): Promise<any> {
return this.delete(`/api/node/mo/${dn}.json`);
}
// Add convenience methods for the server
async listTenants(): Promise<any> {
return this.getTenants();
}
async getTenant(name: string): Promise<any> {
const tenants = await this.getTenants();
const tenant = tenants.imdata?.find((t: any) => t.fvTenant?.attributes?.name === name);
return tenant || null;
}
async deleteTenant(name: string): Promise<void> {
await this.deleteObject(`uni/tn-${name}`);
}
async listApplicationProfiles(tenant: string): Promise<any> {
return this.getApplicationProfiles(tenant);
}
async listEndpointGroups(tenant: string, appProfile: string): Promise<any> {
return this.getEndpointGroups(tenant, appProfile);
}
async listBridgeDomains(tenant: string): Promise<any> {
return this.getBridgeDomains(tenant);
}
async listVRFs(tenant: string): Promise<any> {
return this.getVRFs(tenant);
}
async listContracts(tenant: string): Promise<any> {
return this.getContracts(tenant);
}
async listFaults(severity?: string): Promise<any> {
let url = '/api/node/class/faultInst.json';
if (severity) {
url += `?query-target-filter=eq(faultInst.severity,"${severity}")`;
}
return this.get(url);
}
async listNodes(): Promise<any> {
return this.getNodes();
}
async getSystemInfo(): Promise<any> {
const response = await this.get('/api/node/class/topSystem.json');
return response.imdata?.[0] || null;
}
async logout(): Promise<void> {
try {
await this.post('/api/aaaLogout.json', {});
} catch (error) {
console.warn('Logout request failed:', error);
}
}
}