mock-server.tsā¢17.4 kB
#!/usr/bin/env node
/**
* Mock APIC Server for Testing
*
* This creates a mock APIC server that responds to common ACI API calls
* with realistic test data. Use this for development and testing when
* you don't have access to a real APIC controller.
*/
import * as http from 'http';
import * as url from 'url';
interface MockData {
tenants: any[];
applicationProfiles: any[];
endpointGroups: any[];
bridgeDomains: any[];
vrfs: any[];
contracts: any[];
faults: any[];
nodes: any[];
}
class MockAPICServer {
private server: http.Server;
private port: number;
private sessions: Map<string, { token: string; expires: number }> = new Map();
private mockData!: MockData;
constructor(port: number = 8443) {
this.port = port;
this.initializeMockData();
this.server = http.createServer(this.handleRequest.bind(this));
}
private initializeMockData() {
this.mockData = {
tenants: [
{
fvTenant: {
attributes: {
name: 'common',
dn: 'uni/tn-common',
descr: 'Common tenant'
}
}
},
{
fvTenant: {
attributes: {
name: 'production',
dn: 'uni/tn-production',
descr: 'Production tenant'
}
}
},
{
fvTenant: {
attributes: {
name: 'development',
dn: 'uni/tn-development',
descr: 'Development tenant'
}
}
}
],
applicationProfiles: [
{
fvAp: {
attributes: {
name: 'web-app',
dn: 'uni/tn-production/ap-web-app',
descr: '3-tier web application'
}
}
},
{
fvAp: {
attributes: {
name: 'database-app',
dn: 'uni/tn-production/ap-database-app',
descr: 'Database application'
}
}
}
],
endpointGroups: [
{
fvAEPg: {
attributes: {
name: 'web-epg',
dn: 'uni/tn-production/ap-web-app/epg-web-epg',
descr: 'Web tier EPG'
}
}
},
{
fvAEPg: {
attributes: {
name: 'app-epg',
dn: 'uni/tn-production/ap-web-app/epg-app-epg',
descr: 'Application tier EPG'
}
}
},
{
fvAEPg: {
attributes: {
name: 'db-epg',
dn: 'uni/tn-production/ap-web-app/epg-db-epg',
descr: 'Database tier EPG'
}
}
}
],
bridgeDomains: [
{
fvBD: {
attributes: {
name: 'web-bd',
dn: 'uni/tn-production/BD-web-bd',
descr: 'Web bridge domain'
}
}
},
{
fvBD: {
attributes: {
name: 'app-bd',
dn: 'uni/tn-production/BD-app-bd',
descr: 'Application bridge domain'
}
}
}
],
vrfs: [
{
fvCtx: {
attributes: {
name: 'prod-vrf',
dn: 'uni/tn-production/ctx-prod-vrf',
descr: 'Production VRF'
}
}
},
{
fvCtx: {
attributes: {
name: 'dev-vrf',
dn: 'uni/tn-development/ctx-dev-vrf',
descr: 'Development VRF'
}
}
}
],
contracts: [
{
vzBrCP: {
attributes: {
name: 'web-to-app',
dn: 'uni/tn-production/brc-web-to-app',
descr: 'Web to App contract'
}
}
},
{
vzBrCP: {
attributes: {
name: 'app-to-db',
dn: 'uni/tn-production/brc-app-to-db',
descr: 'App to DB contract'
}
}
}
],
faults: [
{
faultInst: {
attributes: {
severity: 'critical',
descr: 'Power supply failure on node 101',
dn: 'topology/pod-1/node-101/fault-F0467',
code: 'F0467',
created: '2024-11-21T10:30:00.000+00:00'
}
}
},
{
faultInst: {
attributes: {
severity: 'warning',
descr: 'High CPU utilization on node 102',
dn: 'topology/pod-1/node-102/fault-F1234',
code: 'F1234',
created: '2024-11-21T11:15:00.000+00:00'
}
}
},
{
faultInst: {
attributes: {
severity: 'info',
descr: 'Interface flapped on node 103',
dn: 'topology/pod-1/node-103/fault-F5678',
code: 'F5678',
created: '2024-11-21T12:00:00.000+00:00'
}
}
}
],
nodes: [
{
fabricNode: {
attributes: {
name: 'leaf-101',
dn: 'topology/pod-1/node-101',
id: '101',
role: 'leaf',
model: 'N9K-C93180YC-EX',
serial: 'FDO12345678'
}
}
},
{
fabricNode: {
attributes: {
name: 'leaf-102',
dn: 'topology/pod-1/node-102',
id: '102',
role: 'leaf',
model: 'N9K-C93180YC-EX',
serial: 'FDO87654321'
}
}
},
{
fabricNode: {
attributes: {
name: 'spine-201',
dn: 'topology/pod-1/node-201',
id: '201',
role: 'spine',
model: 'N9K-C9336C-FX2',
serial: 'FDO11111111'
}
}
}
]
};
}
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
const parsedUrl = url.parse(req.url || '', true);
const path = parsedUrl.pathname || '';
const method = req.method || 'GET';
console.log(`Mock APIC: ${method} ${path}`);
// Set common headers
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie');
if (method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
try {
this.routeRequest(method, path, req, res);
} catch (error) {
console.error('Mock APIC Error:', error);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}
private routeRequest(method: string, path: string, req: http.IncomingMessage, res: http.ServerResponse) {
// Authentication endpoints
if (path === '/api/aaaLogin.json' && method === 'POST') {
this.handleLogin(req, res);
return;
}
if (path === '/api/aaaRefresh.json' && method === 'GET') {
this.handleCertAuth(req, res);
return;
}
if (path === '/api/aaaLogout.json' && method === 'POST') {
this.handleLogout(req, res);
return;
}
// Check authentication for all other requests
if (!this.isAuthenticated(req)) {
res.writeHead(401);
res.end(JSON.stringify({
imdata: [{
error: {
attributes: {
code: '401',
text: 'Authentication required'
}
}
}]
}));
return;
}
// API endpoints
if (path === '/api/node/class/fvTenant.json') {
this.sendResponse(res, { imdata: this.mockData.tenants });
return;
}
if (path === '/api/node/class/fvAp.json') {
this.sendResponse(res, { imdata: this.mockData.applicationProfiles });
return;
}
if (path === '/api/node/class/fvAEPg.json') {
this.sendResponse(res, { imdata: this.mockData.endpointGroups });
return;
}
if (path === '/api/node/class/fvBD.json') {
this.sendResponse(res, { imdata: this.mockData.bridgeDomains });
return;
}
if (path === '/api/node/class/fvCtx.json') {
this.sendResponse(res, { imdata: this.mockData.vrfs });
return;
}
if (path === '/api/node/class/vzBrCP.json') {
this.sendResponse(res, { imdata: this.mockData.contracts });
return;
}
if (path === '/api/node/class/faultInst.json') {
const query = url.parse(req.url || '', true).query;
let faults = this.mockData.faults;
// Handle severity filter
if (query['query-target-filter']) {
const filter = query['query-target-filter'] as string;
if (filter.includes('severity')) {
const severity = filter.match(/\"([^\"]+)\"/)?.[1];
if (severity) {
faults = this.mockData.faults.filter(fault =>
fault.faultInst.attributes.severity === severity
);
}
}
}
this.sendResponse(res, { imdata: faults });
return;
}
if (path === '/api/node/class/fabricNode.json') {
this.sendResponse(res, { imdata: this.mockData.nodes });
return;
}
if (path === '/api/node/class/fabricHealthTotal.json') {
this.sendResponse(res, {
imdata: [{
fabricHealthTotal: {
attributes: {
cur: '95',
dn: 'topology/health',
healthAvg: '94',
healthMax: '99',
healthMin: '90'
}
}
}]
});
return;
}
if (path === '/api/node/class/topSystem.json') {
this.sendResponse(res, {
imdata: [{
topSystem: {
attributes: {
name: 'Mock APIC',
version: '5.2(7f)',
dn: 'topology/pod-1/node-1/sys',
fabricMAC: '00:22:BD:F8:19:FF'
}
}
}]
});
return;
}
// Handle POST/PUT/DELETE operations
if (method === 'POST' && path.includes('/api/node/mo/')) {
this.handleCreateOrUpdate(req, res);
return;
}
if (method === 'DELETE' && path.includes('/api/node/mo/')) {
this.handleDelete(req, res);
return;
}
// Default 404
res.writeHead(404);
res.end(JSON.stringify({
imdata: [{
error: {
attributes: {
code: '404',
text: `Endpoint not found: ${path}`
}
}
}]
}));
}
private handleLogin(req: http.IncomingMessage, res: http.ServerResponse) {
this.readRequestBody(req, (body) => {
try {
const loginData = JSON.parse(body);
const username = loginData.aaaUser?.attributes?.name;
const password = loginData.aaaUser?.attributes?.pwd;
if (username && password) {
const token = this.generateToken();
const sessionTimeout = 600; // 10 minutes
this.sessions.set(token, {
token: token,
expires: Date.now() + (sessionTimeout * 1000)
});
// Set cookie for session management
res.setHeader('Set-Cookie', `APIC-Cookie=${token}; Path=/; HttpOnly`);
this.sendResponse(res, {
imdata: [{
aaaLogin: {
attributes: {
token: token,
sessionTimeoutSeconds: sessionTimeout.toString(),
maximumLifetimeSeconds: '86400'
}
}
}]
});
} else {
res.writeHead(401);
this.sendResponse(res, {
imdata: [{
error: {
attributes: {
code: '401',
text: 'Invalid credentials'
}
}
}]
});
}
} catch (error) {
res.writeHead(400);
this.sendResponse(res, {
imdata: [{
error: {
attributes: {
code: '400',
text: 'Invalid JSON'
}
}
}]
});
}
});
}
private handleCertAuth(req: http.IncomingMessage, res: http.ServerResponse) {
const certName = req.headers['x-aci-certificate'];
const signature = req.headers['x-aci-signature'];
const timestamp = req.headers['x-aci-timestamp'];
if (certName && signature && timestamp) {
const token = this.generateToken();
const sessionTimeout = 600;
this.sessions.set(token, {
token: token,
expires: Date.now() + (sessionTimeout * 1000)
});
res.setHeader('Set-Cookie', `APIC-Cookie=${token}; Path=/; HttpOnly`);
this.sendResponse(res, {
imdata: [{
aaaLogin: {
attributes: {
token: token,
sessionTimeoutSeconds: sessionTimeout.toString()
}
}
}]
});
} else {
res.writeHead(401);
this.sendResponse(res, {
imdata: [{
error: {
attributes: {
code: '401',
text: 'Certificate authentication failed'
}
}
}]
});
}
}
private handleLogout(req: http.IncomingMessage, res: http.ServerResponse) {
const cookie = req.headers.cookie;
if (cookie) {
const tokenMatch = cookie.match(/APIC-Cookie=([^;]+)/);
if (tokenMatch) {
this.sessions.delete(tokenMatch[1]);
}
}
this.sendResponse(res, { imdata: [] });
}
private handleCreateOrUpdate(req: http.IncomingMessage, res: http.ServerResponse) {
this.readRequestBody(req, (body) => {
try {
const data = JSON.parse(body);
console.log('Mock APIC: Creating/updating object:', JSON.stringify(data, null, 2));
// Simulate successful creation
this.sendResponse(res, { imdata: [] });
} catch (error) {
res.writeHead(400);
this.sendResponse(res, {
imdata: [{
error: {
attributes: {
code: '400',
text: 'Invalid JSON data'
}
}
}]
});
}
});
}
private handleDelete(req: http.IncomingMessage, res: http.ServerResponse) {
console.log('Mock APIC: Deleting object:', req.url);
this.sendResponse(res, { imdata: [] });
}
private isAuthenticated(req: http.IncomingMessage): boolean {
const cookie = req.headers.cookie;
if (!cookie) return false;
const tokenMatch = cookie.match(/APIC-Cookie=([^;]+)/);
if (!tokenMatch) return false;
const session = this.sessions.get(tokenMatch[1]);
if (!session || session.expires < Date.now()) {
if (session) this.sessions.delete(tokenMatch[1]);
return false;
}
return true;
}
private generateToken(): string {
return 'mock-token-' + Math.random().toString(36).substring(2) + Date.now();
}
private readRequestBody(req: http.IncomingMessage, callback: (body: string) => void) {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
callback(body);
});
}
private sendResponse(res: http.ServerResponse, data: any) {
res.writeHead(200);
res.end(JSON.stringify(data, null, 2));
}
public start() {
this.server.listen(this.port, () => {
console.log(`š Mock APIC Server running on port ${this.port}`);
console.log(`š” Base URL: http://localhost:${this.port}`);
console.log(`š Use any username/password to login`);
console.log(`š Available endpoints:`);
console.log(` - POST /api/aaaLogin.json (login)`);
console.log(` - GET /api/node/class/fvTenant.json (tenants)`);
console.log(` - GET /api/node/class/faultInst.json (faults)`);
console.log(` - GET /api/node/class/fabricHealthTotal.json (health)`);
console.log(` - GET /api/node/class/fabricNode.json (nodes)`);
console.log(` - ... and more!`);
console.log(``);
console.log(`š§ Test with: curl -X POST http://localhost:${this.port}/api/aaaLogin.json \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(` -d '{"aaaUser":{"attributes":{"name":"admin","pwd":"admin"}}}'`);
});
this.server.on('error', (error) => {
console.error('Mock APIC Server error:', error);
});
}
public stop() {
this.server.close();
}
}
// Start the mock server
if (import.meta.url === `file://${process.argv[1]}`) {
const port = parseInt(process.env.MOCK_APIC_PORT || '8443');
const mockServer = new MockAPICServer(port);
mockServer.start();
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nš Shutting down Mock APIC Server...');
mockServer.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nš Shutting down Mock APIC Server...');
mockServer.stop();
process.exit(0);
});
}
export { MockAPICServer };