cisco-axl 2.1.0 → 2.1.2
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/.github/workflows/release.yml +6 -6
- package/README.md +42 -303
- package/claude-hooks.json +30 -0
- package/cli/commands/add.js +1 -1
- package/cli/commands/doctor.js +134 -0
- package/cli/commands/execute.js +1 -1
- package/cli/commands/remove.js +1 -1
- package/cli/commands/sql.js +1 -1
- package/cli/commands/update.js +1 -1
- package/cli/index.js +11 -2
- package/cli/utils/confirm.js +34 -0
- package/cli/utils/readonly.js +13 -18
- package/cli/utils/wordlist.js +9 -0
- package/docs/api.md +191 -0
- package/docs/claude-code-hooks.md +85 -0
- package/docs/cli.md +109 -0
- package/package.json +2 -2
- package/skills/cisco-axl-cli/SKILL.md +22 -15
|
@@ -3,11 +3,11 @@ name: Release
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
-
|
|
6
|
+
- "v*"
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
|
-
id-token: write
|
|
10
|
-
contents: write
|
|
9
|
+
id-token: write # Required for OIDC authentication
|
|
10
|
+
contents: write # Required for creating GitHub Release
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
13
|
publish:
|
|
@@ -19,8 +19,8 @@ jobs:
|
|
|
19
19
|
- name: Setup Node.js
|
|
20
20
|
uses: actions/setup-node@v4
|
|
21
21
|
with:
|
|
22
|
-
node-version:
|
|
23
|
-
registry-url:
|
|
22
|
+
node-version: "22"
|
|
23
|
+
registry-url: "https://registry.npmjs.org"
|
|
24
24
|
|
|
25
25
|
- name: Update npm
|
|
26
26
|
run: npm install -g npm@latest
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
run: node -e "const axlService = require('./dist/index.js'); console.log('CJS require OK');"
|
|
42
42
|
|
|
43
43
|
- name: Publish to npm
|
|
44
|
-
run: npm publish --access public
|
|
44
|
+
run: npm publish --access public --provenance
|
|
45
45
|
|
|
46
46
|
- name: Create GitHub Release
|
|
47
47
|
uses: softprops/action-gh-release@v2
|
package/README.md
CHANGED
|
@@ -42,11 +42,7 @@ If you are using self-signed certificates on Cisco VOS products you may need to
|
|
|
42
42
|
|
|
43
43
|
Supported CUCM versions: `11.0`, `11.5`, `12.0`, `12.5`, `14.0`, `15.0`
|
|
44
44
|
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
The CLI provides full AXL access from the command line — CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and a raw execute escape hatch for any AXL operation.
|
|
48
|
-
|
|
49
|
-
### Quick Start
|
|
45
|
+
## Quick Start
|
|
50
46
|
|
|
51
47
|
```bash
|
|
52
48
|
# Configure a cluster
|
|
@@ -59,27 +55,31 @@ cisco-axl config test
|
|
|
59
55
|
cisco-axl list Phone --search "name=SEP%"
|
|
60
56
|
|
|
61
57
|
# Get a specific phone
|
|
62
|
-
cisco-axl get Phone SEP001122334455
|
|
58
|
+
cisco-axl get Phone SEP001122334455
|
|
63
59
|
|
|
64
60
|
# SQL query
|
|
65
61
|
cisco-axl sql query "SELECT name, description FROM device WHERE name LIKE 'SEP%'"
|
|
62
|
+
```
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
cisco-axl operations --filter phone
|
|
69
|
-
cisco-axl operations --type action --filter phone
|
|
70
|
-
|
|
71
|
-
# Describe what tags an operation needs
|
|
72
|
-
cisco-axl describe getPhone --detailed
|
|
64
|
+
## Configuration
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
cisco-axl
|
|
66
|
+
```bash
|
|
67
|
+
cisco-axl config add <name> --host <host> --username <user> --password <pass> --cucm-version <ver> --insecure
|
|
68
|
+
cisco-axl config use <name> # switch active cluster
|
|
69
|
+
cisco-axl config list # list all clusters
|
|
70
|
+
cisco-axl config show # show active cluster (masks passwords)
|
|
71
|
+
cisco-axl config remove <name> # remove a cluster
|
|
72
|
+
cisco-axl config test # test connectivity
|
|
76
73
|
```
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
Auth precedence: CLI flags > env vars (`CUCM_HOST`, `CUCM_USERNAME`, `CUCM_PASSWORD`, `CUCM_VERSION`) > config file.
|
|
76
|
+
|
|
77
|
+
Config stored at `~/.cisco-axl/config.json`. Supports [ss-cli](https://github.com/sieteunoseis/ss-cli) `<ss:ID:field>` placeholders.
|
|
78
|
+
|
|
79
|
+
## CLI Commands
|
|
79
80
|
|
|
80
81
|
| Command | Description |
|
|
81
82
|
|---------|-------------|
|
|
82
|
-
| `config add/use/list/show/remove/test` | Manage multi-cluster configurations |
|
|
83
83
|
| `get <type> <identifier>` | Get a single item |
|
|
84
84
|
| `list <type>` | List items with search, pagination, returned tags |
|
|
85
85
|
| `add <type>` | Add an item (inline JSON, template, or bulk CSV) |
|
|
@@ -87,313 +87,52 @@ cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
|
|
|
87
87
|
| `remove <type> <identifier>` | Remove an item |
|
|
88
88
|
| `sql query/update` | Execute SQL against CUCM |
|
|
89
89
|
| `execute <operation>` | Run any raw AXL operation |
|
|
90
|
-
| `operations` | List available operations
|
|
91
|
-
| `describe <operation>` | Show tag schema
|
|
92
|
-
|
|
93
|
-
### Configuration
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
# Multiple clusters
|
|
97
|
-
cisco-axl config add lab --host 10.0.0.1 --username admin --password secret --cucm-version 14.0 --insecure
|
|
98
|
-
cisco-axl config add prod --host 10.0.0.2 --username axladmin --password secret --cucm-version 15.0 --insecure
|
|
99
|
-
cisco-axl config use prod
|
|
100
|
-
cisco-axl config list
|
|
101
|
-
|
|
102
|
-
# Per-command cluster override
|
|
103
|
-
cisco-axl list Phone --search "name=SEP%" --cluster lab
|
|
104
|
-
|
|
105
|
-
# Environment variables (CI/CD, AI agents)
|
|
106
|
-
export CUCM_HOST=10.0.0.1 CUCM_USERNAME=admin CUCM_PASSWORD=secret CUCM_VERSION=14.0
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Config stored at `~/.cisco-axl/config.json`. Supports optional [Secret Server](https://github.com/sieteunoseis/ss-cli) integration via `<ss:ID:field>` placeholders.
|
|
110
|
-
|
|
111
|
-
### Output Formats
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
cisco-axl list Phone --search "name=SEP%" --format table # default, human-readable
|
|
115
|
-
cisco-axl list Phone --search "name=SEP%" --format json # structured JSON
|
|
116
|
-
cisco-axl list Phone --search "name=SEP%" --format toon # token-efficient for AI agents
|
|
117
|
-
cisco-axl list Phone --search "name=SEP%" --format csv # spreadsheet export
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Bulk Operations from CSV
|
|
121
|
-
|
|
122
|
-
Requires optional packages: `npm install json-variables csv-parse`
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
# Bulk add phones from template + CSV
|
|
126
|
-
cisco-axl add Phone --template phone-template.json --csv phones.csv
|
|
127
|
-
cisco-axl add Phone --template phone-template.json --csv phones.csv --dry-run # preview first
|
|
128
|
-
|
|
129
|
-
# Single template with inline vars
|
|
130
|
-
cisco-axl add Phone --template phone-template.json --vars '{"mac":"001122334455","dp":"DP_HQ"}'
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Template file (`phone-template.json`):
|
|
134
|
-
```json
|
|
135
|
-
{
|
|
136
|
-
"name": "SEP%%mac%%",
|
|
137
|
-
"devicePoolName": "%%devicePool%%",
|
|
138
|
-
"description": "%%description%%",
|
|
139
|
-
"protocol": "SIP"
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Global Flags
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
--format table|json|toon|csv Output format (default: table)
|
|
147
|
-
--insecure Skip TLS certificate verification
|
|
148
|
-
--clean Remove empty/null values from results
|
|
149
|
-
--no-attributes Remove XML attributes from results
|
|
150
|
-
--read-only Restrict to read-only operations
|
|
151
|
-
--no-audit Disable audit logging for this command
|
|
152
|
-
--debug Enable debug logging
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Command Chaining
|
|
156
|
-
|
|
157
|
-
Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
# Create a partition, CSS, and line in order
|
|
161
|
-
cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
|
|
162
|
-
cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
|
|
163
|
-
cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Piping with --stdin
|
|
167
|
-
|
|
168
|
-
Use `--stdin` to pipe JSON between commands or from other tools like `jq`:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
# Get a phone's config, modify it with jq, update it
|
|
172
|
-
cisco-axl get Phone SEP001122334455 --format json | \
|
|
173
|
-
jq '.description = "Updated via pipe"' | \
|
|
174
|
-
cisco-axl update Phone SEP001122334455 --stdin
|
|
175
|
-
|
|
176
|
-
# Pipe JSON from a file
|
|
177
|
-
cat phone-config.json | cisco-axl add Phone --stdin
|
|
178
|
-
|
|
179
|
-
# Discover tags, fill them in, execute
|
|
180
|
-
cisco-axl describe applyPhone --format json | \
|
|
181
|
-
jq '.name = "SEP001122334455"' | \
|
|
182
|
-
cisco-axl execute applyPhone --stdin
|
|
183
|
-
```
|
|
90
|
+
| `operations` | List available operations |
|
|
91
|
+
| `describe <operation>` | Show tag schema for an operation |
|
|
92
|
+
| `doctor` | Check AXL connectivity and health |
|
|
184
93
|
|
|
185
|
-
|
|
94
|
+
See [full CLI reference](docs/cli.md) for bulk CSV, command chaining, piping with `--stdin`, and operation discovery.
|
|
186
95
|
|
|
187
|
-
|
|
96
|
+
## Global Flags
|
|
188
97
|
|
|
189
|
-
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `--format table\|json\|toon\|csv` | Output format (default: table) |
|
|
101
|
+
| `--insecure` | Skip TLS certificate verification |
|
|
102
|
+
| `--clean` | Remove empty/null values from results |
|
|
103
|
+
| `--no-attributes` | Remove XML attributes from results |
|
|
104
|
+
| `--read-only` | Restrict to read-only operations |
|
|
105
|
+
| `--no-audit` | Disable audit logging for this command |
|
|
106
|
+
| `--debug` | Enable debug logging |
|
|
190
107
|
|
|
191
108
|
## Library API
|
|
192
109
|
|
|
193
|
-
### Setup
|
|
194
|
-
|
|
195
110
|
```javascript
|
|
196
111
|
const axlService = require("cisco-axl");
|
|
112
|
+
const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
197
113
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// With options
|
|
201
|
-
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
202
|
-
logging: { level: "info" },
|
|
203
|
-
retry: { retries: 3, retryDelay: 1000 }
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Logging
|
|
208
|
-
|
|
209
|
-
```javascript
|
|
210
|
-
// Via environment variable
|
|
211
|
-
// DEBUG=true
|
|
212
|
-
|
|
213
|
-
// Via constructor
|
|
214
|
-
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
215
|
-
logging: {
|
|
216
|
-
level: "info", // "error" | "warn" | "info" | "debug"
|
|
217
|
-
handler: (level, message, data) => {
|
|
218
|
-
myLogger[level](message, data);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// Change at runtime
|
|
224
|
-
service.setLogLevel("debug");
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Convenience Methods
|
|
228
|
-
|
|
229
|
-
```javascript
|
|
230
|
-
// Get a single item by name or UUID
|
|
114
|
+
// Get, list, add, update, remove
|
|
231
115
|
await service.getItem("Phone", "SEP001122334455");
|
|
232
|
-
await service.
|
|
233
|
-
|
|
234
|
-
// List items with search criteria and returned tags
|
|
235
|
-
await service.listItems("RoutePartition"); // all partitions
|
|
236
|
-
await service.listItems("Phone", { name: "SEP%" }, { name: "", model: "" });
|
|
237
|
-
|
|
238
|
-
// Add, update, remove
|
|
116
|
+
await service.listItems("Phone", { name: "SEP%" });
|
|
239
117
|
await service.addItem("RoutePartition", { name: "NEW-PT", description: "New" });
|
|
240
|
-
await service.updateItem("Phone", "SEP001122334455", { description: "Updated" });
|
|
241
|
-
await service.removeItem("RoutePartition", "NEW-PT");
|
|
242
118
|
|
|
243
119
|
// SQL
|
|
244
|
-
|
|
245
|
-
await service.executeSqlUpdate("UPDATE routepartition SET description='test' WHERE name='NEW-PT'");
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### Operation Discovery
|
|
120
|
+
await service.executeSqlQuery("SELECT name FROM routepartition");
|
|
249
121
|
|
|
250
|
-
|
|
251
|
-
// List all operations
|
|
252
|
-
const ops = await service.returnOperations();
|
|
253
|
-
const phoneOps = await service.returnOperations("phone");
|
|
254
|
-
|
|
255
|
-
// Get tag schema
|
|
256
|
-
const tags = await service.getOperationTags("addRoutePartition");
|
|
257
|
-
|
|
258
|
-
// Get detailed metadata (required, nillable, type)
|
|
259
|
-
const detailed = await service.getOperationTagsDetailed("addRoutePartition");
|
|
260
|
-
console.log(detailed.routePartition.required); // true
|
|
261
|
-
console.log(detailed.routePartition.children.name.type); // "string"
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Execute Any Operation
|
|
265
|
-
|
|
266
|
-
```javascript
|
|
122
|
+
// Any AXL operation
|
|
267
123
|
const tags = await service.getOperationTags("addRoutePartition");
|
|
268
124
|
tags.routePartition.name = "INTERNAL-PT";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const result = await service.executeOperation("addRoutePartition", tags);
|
|
272
|
-
console.log("UUID:", result);
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Batch Operations
|
|
276
|
-
|
|
277
|
-
```javascript
|
|
278
|
-
const results = await service.executeBatch([
|
|
279
|
-
{ operation: "getPhone", tags: { name: "SEP001122334455" } },
|
|
280
|
-
{ operation: "getPhone", tags: { name: "SEP556677889900" } },
|
|
281
|
-
], 5); // concurrency limit
|
|
282
|
-
|
|
283
|
-
results.forEach((r) => {
|
|
284
|
-
console.log(r.success ? `${r.operation}: OK` : `${r.operation}: ${r.error.message}`);
|
|
285
|
-
});
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Error Handling
|
|
289
|
-
|
|
290
|
-
```javascript
|
|
291
|
-
const { AXLAuthError, AXLNotFoundError, AXLOperationError, AXLValidationError } = require("cisco-axl");
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
await service.executeOperation("getPhone", { name: "INVALID" });
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error instanceof AXLAuthError) console.log("Bad credentials");
|
|
297
|
-
else if (error instanceof AXLNotFoundError) console.log("Operation not found:", error.operation);
|
|
298
|
-
else if (error instanceof AXLOperationError) console.log("SOAP fault:", error.message);
|
|
299
|
-
else if (error instanceof AXLValidationError) console.log("Invalid input:", error.message);
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Retry Configuration
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
let service = new axlService("10.10.20.1", "admin", "pass", "14.0", {
|
|
307
|
-
retry: {
|
|
308
|
-
retries: 3,
|
|
309
|
-
retryDelay: 1000,
|
|
310
|
-
retryOn: (error) => error.message.includes("ECONNRESET")
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### ESM Support
|
|
316
|
-
|
|
317
|
-
```javascript
|
|
318
|
-
// CommonJS
|
|
319
|
-
const axlService = require("cisco-axl");
|
|
320
|
-
|
|
321
|
-
// ESM
|
|
322
|
-
import axlService from "cisco-axl";
|
|
323
|
-
import { AXLAuthError, AXLOperationError } from "cisco-axl";
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### json-variables Support
|
|
327
|
-
|
|
328
|
-
```javascript
|
|
329
|
-
var lineTemplate = {
|
|
330
|
-
pattern: "%%_extension_%%",
|
|
331
|
-
alertingName: "%%_firstName_%% %%_lastName_%%",
|
|
332
|
-
description: "%%_firstName_%% %%_lastName_%%",
|
|
333
|
-
_data: {
|
|
334
|
-
extension: "1001",
|
|
335
|
-
firstName: "Tom",
|
|
336
|
-
lastName: "Smith",
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const lineTags = jVar(lineTemplate);
|
|
341
|
-
await service.executeOperation("updateLine", lineTags);
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
## Methods Reference
|
|
345
|
-
|
|
346
|
-
### Core
|
|
347
|
-
|
|
348
|
-
| Method | Description |
|
|
349
|
-
|--------|-------------|
|
|
350
|
-
| `new axlService(host, user, pass, version, opts?)` | Constructor |
|
|
351
|
-
| `testAuthentication()` | Test credentials against AXL endpoint |
|
|
352
|
-
| `returnOperations(filter?)` | List available operations |
|
|
353
|
-
| `getOperationTags(operation)` | Get tag schema for an operation |
|
|
354
|
-
| `getOperationTagsDetailed(operation)` | Get detailed tag metadata (required/nillable/type) |
|
|
355
|
-
| `executeOperation(operation, tags, opts?)` | Execute any AXL operation |
|
|
356
|
-
| `executeBatch(operations[], concurrency?)` | Parallel batch execution |
|
|
357
|
-
| `setLogLevel(level)` | Change log level at runtime |
|
|
358
|
-
|
|
359
|
-
### Convenience
|
|
360
|
-
|
|
361
|
-
| Method | Description |
|
|
362
|
-
|--------|-------------|
|
|
363
|
-
| `getItem(type, identifier, opts?)` | Get single item by name or UUID |
|
|
364
|
-
| `listItems(type, search?, returnedTags?, opts?)` | List items with filtering |
|
|
365
|
-
| `addItem(type, data, opts?)` | Add a new item |
|
|
366
|
-
| `updateItem(type, identifier, updates, opts?)` | Update an existing item |
|
|
367
|
-
| `removeItem(type, identifier, opts?)` | Remove an item |
|
|
368
|
-
| `executeSqlQuery(sql)` | Run a SQL SELECT query |
|
|
369
|
-
| `executeSqlUpdate(sql)` | Run a SQL INSERT/UPDATE/DELETE |
|
|
370
|
-
|
|
371
|
-
## Examples
|
|
372
|
-
|
|
373
|
-
Check the **examples** folder for different ways to use this library.
|
|
374
|
-
|
|
375
|
-
Run the integration tests against a CUCM cluster:
|
|
376
|
-
|
|
377
|
-
```bash
|
|
378
|
-
npm run staging
|
|
125
|
+
await service.executeOperation("addRoutePartition", tags);
|
|
379
126
|
```
|
|
380
127
|
|
|
381
|
-
|
|
128
|
+
See [full API documentation](docs/api.md) for all methods, error handling, batch operations, TypeScript, retry config, and logging.
|
|
382
129
|
|
|
383
|
-
|
|
384
|
-
import axlService from 'cisco-axl';
|
|
385
|
-
|
|
386
|
-
const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
387
|
-
|
|
388
|
-
const tags = await service.getOperationTags("listRoutePartition");
|
|
389
|
-
tags.searchCriteria.name = "%%";
|
|
390
|
-
const result = await service.executeOperation("listRoutePartition", tags);
|
|
391
|
-
```
|
|
130
|
+
## Giving Back
|
|
392
131
|
|
|
393
|
-
|
|
132
|
+
If you found this helpful, consider:
|
|
394
133
|
|
|
395
|
-
|
|
134
|
+
[](https://buymeacoffee.com/automatebldrs)
|
|
396
135
|
|
|
397
|
-
|
|
136
|
+
## License
|
|
398
137
|
|
|
399
|
-
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cisco-axl",
|
|
3
|
+
"description": "Hooks for Cisco CUCM AXL CLI",
|
|
4
|
+
"author": "sieteunoseis",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "Block write operations",
|
|
9
|
+
"description": "Blocks add, update, remove, execute, and sql update commands",
|
|
10
|
+
"default": true,
|
|
11
|
+
"event": "PreToolUse",
|
|
12
|
+
"matcher": "Bash",
|
|
13
|
+
"hook": {
|
|
14
|
+
"type": "command",
|
|
15
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl (add|update|remove|execute|sql update)'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl write operation. Use --read-only or get explicit user approval.\"}'; fi; }"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "Enforce --read-only flag",
|
|
20
|
+
"description": "Requires --read-only on every cisco-axl command",
|
|
21
|
+
"default": false,
|
|
22
|
+
"event": "PreToolUse",
|
|
23
|
+
"matcher": "Bash",
|
|
24
|
+
"hook": {
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl ' && ! echo \"$cmd\" | grep -q '\\-\\-read-only'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl must be run with --read-only. Retry with the flag.\"}'; fi; }"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
package/cli/commands/add.js
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { loadConfig, getConfigPath, getConfigDir } = require("../utils/config.js");
|
|
4
|
+
const { resolveConfig } = require("../utils/connection.js");
|
|
5
|
+
|
|
6
|
+
module.exports = function (program) {
|
|
7
|
+
program.command("doctor")
|
|
8
|
+
.description("Check AXL connectivity and configuration health")
|
|
9
|
+
.action(async (opts, command) => {
|
|
10
|
+
const globalOpts = command.optsWithGlobals();
|
|
11
|
+
let passed = 0;
|
|
12
|
+
let warned = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
const ok = (msg) => { console.log(` ✓ ${msg}`); passed++; };
|
|
16
|
+
const warn = (msg) => { console.log(` ⚠ ${msg}`); warned++; };
|
|
17
|
+
const fail = (msg) => { console.log(` ✗ ${msg}`); failed++; };
|
|
18
|
+
|
|
19
|
+
console.log("\n cisco-axl doctor");
|
|
20
|
+
console.log(" " + "─".repeat(50));
|
|
21
|
+
|
|
22
|
+
// 1. Configuration
|
|
23
|
+
console.log("\n Configuration");
|
|
24
|
+
let conn;
|
|
25
|
+
try {
|
|
26
|
+
const data = loadConfig();
|
|
27
|
+
if (!data.activeCluster) {
|
|
28
|
+
fail("No active cluster configured");
|
|
29
|
+
console.log(" Run: cisco-axl config add <name> --host <host> --username <user> --password <pass> --cucm-version <ver>");
|
|
30
|
+
printSummary(passed, warned, failed);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
ok(`Active cluster: ${data.activeCluster}`);
|
|
34
|
+
const cluster = data.clusters[data.activeCluster];
|
|
35
|
+
ok(`Host: ${cluster.host}`);
|
|
36
|
+
ok(`Username: ${cluster.username}`);
|
|
37
|
+
ok(`CUCM version: ${cluster.version}`);
|
|
38
|
+
|
|
39
|
+
if (cluster.insecure) warn("TLS verification: disabled (--insecure)");
|
|
40
|
+
else ok("TLS verification: enabled");
|
|
41
|
+
|
|
42
|
+
conn = await resolveConfig(globalOpts);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
fail(`Config error: ${err.message}`);
|
|
45
|
+
printSummary(passed, warned, failed);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. AXL API connectivity
|
|
50
|
+
console.log("\n AXL API");
|
|
51
|
+
try {
|
|
52
|
+
if (conn.insecure) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; }
|
|
53
|
+
const axlService = require("../../dist/index.js");
|
|
54
|
+
const service = new axlService(conn.host, conn.username, conn.password, conn.version);
|
|
55
|
+
|
|
56
|
+
await service.testAuthentication();
|
|
57
|
+
ok(`AXL API: connected`);
|
|
58
|
+
|
|
59
|
+
// Try a simple SQL query to verify read access
|
|
60
|
+
try {
|
|
61
|
+
const sqlResult = await service.executeSqlQuery("SELECT COUNT(*) AS cnt FROM device");
|
|
62
|
+
const rows = Array.isArray(sqlResult) ? sqlResult : sqlResult?.row || [];
|
|
63
|
+
const count = rows[0]?.cnt || "?";
|
|
64
|
+
ok(`SQL query: ${count} device(s) in database`);
|
|
65
|
+
} catch {
|
|
66
|
+
warn("SQL query: could not query device table — may lack permissions");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check operation count
|
|
70
|
+
try {
|
|
71
|
+
const ops = await service.returnOperations();
|
|
72
|
+
const count = Array.isArray(ops) ? ops.length : 0;
|
|
73
|
+
ok(`AXL operations: ${count} available`);
|
|
74
|
+
} catch {
|
|
75
|
+
warn("Could not list AXL operations");
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const msg = err.message || String(err);
|
|
79
|
+
if (msg.includes("401") || msg.includes("Authentication") || msg.includes("Unauthorized")) {
|
|
80
|
+
fail("AXL API: authentication failed — check username/password");
|
|
81
|
+
} else if (msg.includes("ECONNREFUSED")) {
|
|
82
|
+
fail("AXL API: connection refused — check host and port");
|
|
83
|
+
} else if (msg.includes("ENOTFOUND")) {
|
|
84
|
+
fail("AXL API: hostname not found — check host");
|
|
85
|
+
} else if (msg.includes("certificate")) {
|
|
86
|
+
fail("AXL API: TLS certificate error — try adding --insecure to the cluster config");
|
|
87
|
+
} else {
|
|
88
|
+
fail(`AXL API: ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Security
|
|
93
|
+
console.log("\n Security");
|
|
94
|
+
try {
|
|
95
|
+
const fs = require("node:fs");
|
|
96
|
+
const configPath = getConfigPath();
|
|
97
|
+
const stats = fs.statSync(configPath);
|
|
98
|
+
const mode = (stats.mode & 0o777).toString(8);
|
|
99
|
+
if (mode === "600") ok(`Config file permissions: ${mode} (secure)`);
|
|
100
|
+
else warn(`Config file permissions: ${mode} — should be 600. Run: chmod 600 ${configPath}`);
|
|
101
|
+
} catch { /* config file may not exist yet */ }
|
|
102
|
+
|
|
103
|
+
// 4. Audit trail
|
|
104
|
+
try {
|
|
105
|
+
const fs = require("node:fs");
|
|
106
|
+
const path = require("node:path");
|
|
107
|
+
const auditPath = path.join(getConfigDir(), "audit.jsonl");
|
|
108
|
+
if (fs.existsSync(auditPath)) {
|
|
109
|
+
const stats = fs.statSync(auditPath);
|
|
110
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
111
|
+
ok(`Audit trail: ${sizeMB}MB`);
|
|
112
|
+
if (stats.size > 8 * 1024 * 1024) warn("Audit trail approaching 10MB rotation limit");
|
|
113
|
+
} else {
|
|
114
|
+
ok("Audit trail: empty (no operations logged yet)");
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
|
|
118
|
+
printSummary(passed, warned, failed);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function printSummary(passed, warned, failed) {
|
|
122
|
+
console.log("\n " + "─".repeat(50));
|
|
123
|
+
console.log(` Results: ${passed} passed, ${warned} warning${warned !== 1 ? "s" : ""}, ${failed} failed`);
|
|
124
|
+
if (failed > 0) {
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
console.log(" Status: issues found — review failures above");
|
|
127
|
+
} else if (warned > 0) {
|
|
128
|
+
console.log(" Status: healthy with warnings");
|
|
129
|
+
} else {
|
|
130
|
+
console.log(" Status: all systems healthy");
|
|
131
|
+
}
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
};
|
package/cli/commands/execute.js
CHANGED
package/cli/commands/remove.js
CHANGED
package/cli/commands/sql.js
CHANGED
package/cli/commands/update.js
CHANGED
package/cli/index.js
CHANGED
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
const { Command } = require("commander");
|
|
4
4
|
const pkg = require("../package.json");
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Suppress Node.js TLS warning when --insecure is used
|
|
7
|
+
const originalEmitWarning = process.emitWarning;
|
|
8
|
+
process.emitWarning = (warning, ...args) => {
|
|
9
|
+
if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
10
|
+
originalEmitWarning.call(process, warning, ...args);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const updateNotifier = require("update-notifier").default || require("update-notifier");
|
|
7
15
|
updateNotifier({ pkg }).notify();
|
|
8
|
-
}
|
|
16
|
+
} catch {};
|
|
9
17
|
|
|
10
18
|
const program = new Command();
|
|
11
19
|
|
|
@@ -37,6 +45,7 @@ require("./commands/sql.js")(program);
|
|
|
37
45
|
require("./commands/execute.js")(program);
|
|
38
46
|
require("./commands/operations.js")(program);
|
|
39
47
|
require("./commands/describe.js")(program);
|
|
48
|
+
require("./commands/doctor.js")(program);
|
|
40
49
|
// require("./commands/phone.js")(program);
|
|
41
50
|
// require("./commands/line.js")(program);
|
|
42
51
|
// require("./commands/user.js")(program);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
const { getRandomWord } = require("./wordlist.js");
|
|
3
|
+
|
|
4
|
+
function checkWriteAllowed(clusterConfig, globalOpts = {}) {
|
|
5
|
+
const readOnly = clusterConfig?.readOnly || globalOpts.readOnly;
|
|
6
|
+
if (!readOnly) return Promise.resolve(true);
|
|
7
|
+
|
|
8
|
+
if (!process.stdin.isTTY) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"This cluster is configured as read-only. " +
|
|
11
|
+
"Interactive TTY required for write confirmation. " +
|
|
12
|
+
"Change config with: cisco-axl config update <name> --no-read-only"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const word = getRandomWord();
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
rl.question(
|
|
21
|
+
`\n⚠ This cluster is configured as read-only.\nTo proceed, type "${word}" to confirm: `,
|
|
22
|
+
(answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
if (answer.trim().toLowerCase() === word.toLowerCase()) {
|
|
25
|
+
resolve({ confirmed: true, word });
|
|
26
|
+
} else {
|
|
27
|
+
reject(new Error("Confirmation failed. Write operation cancelled."));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { checkWriteAllowed };
|
package/cli/utils/readonly.js
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { checkWriteAllowed } = require("./confirm.js");
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Check if read-only mode is active (via --read-only flag or cluster config).
|
|
5
|
-
*
|
|
7
|
+
* If read-only, requires interactive TTY confirmation with a random word.
|
|
8
|
+
* Non-interactive sessions (AI agents without TTY) fail automatically.
|
|
6
9
|
*
|
|
7
10
|
* @param {object} globalOpts - Commander global options
|
|
8
11
|
* @param {string} operation - The operation being attempted (for error message)
|
|
9
12
|
*/
|
|
10
|
-
function enforceReadOnly(globalOpts, operation) {
|
|
11
|
-
if (globalOpts.readOnly) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
`Operation "${operation}" blocked — read-only mode is active.\n` +
|
|
14
|
-
"Read-only mode only allows: get, list, describe, operations, sql query.\n" +
|
|
15
|
-
"Remove --read-only flag to perform write operations."
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Also check cluster config for readOnly setting
|
|
13
|
+
async function enforceReadOnly(globalOpts, operation) {
|
|
20
14
|
const { getActiveCluster } = require("./config.js");
|
|
21
15
|
const cluster = getActiveCluster(globalOpts.cluster);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
const clusterConfig = cluster || {};
|
|
17
|
+
|
|
18
|
+
// Build a combined config for checkWriteAllowed
|
|
19
|
+
const config = {
|
|
20
|
+
readOnly: globalOpts.readOnly || clusterConfig.readOnly || false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await checkWriteAllowed(config, globalOpts);
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
module.exports = { enforceReadOnly };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
function getRandomWord() {
|
|
4
|
+
// Generate a random 8-char string that doesn't exist in the codebase
|
|
5
|
+
// Not guessable, not brute-forceable from a known word list
|
|
6
|
+
return crypto.randomBytes(4).toString("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { getRandomWord };
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Library API Reference
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
const axlService = require("cisco-axl");
|
|
7
|
+
|
|
8
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
9
|
+
|
|
10
|
+
// With options
|
|
11
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
12
|
+
logging: { level: "info" },
|
|
13
|
+
retry: { retries: 3, retryDelay: 1000 }
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## ESM / TypeScript
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// CommonJS
|
|
21
|
+
const axlService = require("cisco-axl");
|
|
22
|
+
|
|
23
|
+
// ESM
|
|
24
|
+
import axlService from "cisco-axl";
|
|
25
|
+
import { AXLAuthError, AXLOperationError } from "cisco-axl";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import axlService from 'cisco-axl';
|
|
30
|
+
|
|
31
|
+
const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
32
|
+
|
|
33
|
+
const tags = await service.getOperationTags("listRoutePartition");
|
|
34
|
+
tags.searchCriteria.name = "%%";
|
|
35
|
+
const result = await service.executeOperation("listRoutePartition", tags);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the `examples/typescript` directory for more examples.
|
|
39
|
+
|
|
40
|
+
## Logging
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Via environment variable
|
|
44
|
+
// DEBUG=true
|
|
45
|
+
|
|
46
|
+
// Via constructor
|
|
47
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
48
|
+
logging: {
|
|
49
|
+
level: "info", // "error" | "warn" | "info" | "debug"
|
|
50
|
+
handler: (level, message, data) => {
|
|
51
|
+
myLogger[level](message, data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Change at runtime
|
|
57
|
+
service.setLogLevel("debug");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Convenience Methods
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// Get a single item by name or UUID
|
|
64
|
+
await service.getItem("Phone", "SEP001122334455");
|
|
65
|
+
await service.getItem("Phone", { uuid: "abc-123" });
|
|
66
|
+
|
|
67
|
+
// List items with search criteria and returned tags
|
|
68
|
+
await service.listItems("RoutePartition"); // all partitions
|
|
69
|
+
await service.listItems("Phone", { name: "SEP%" }, { name: "", model: "" });
|
|
70
|
+
|
|
71
|
+
// Add, update, remove
|
|
72
|
+
await service.addItem("RoutePartition", { name: "NEW-PT", description: "New" });
|
|
73
|
+
await service.updateItem("Phone", "SEP001122334455", { description: "Updated" });
|
|
74
|
+
await service.removeItem("RoutePartition", "NEW-PT");
|
|
75
|
+
|
|
76
|
+
// SQL
|
|
77
|
+
const rows = await service.executeSqlQuery("SELECT name FROM routepartition");
|
|
78
|
+
await service.executeSqlUpdate("UPDATE routepartition SET description='test' WHERE name='NEW-PT'");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Operation Discovery
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// List all operations
|
|
85
|
+
const ops = await service.returnOperations();
|
|
86
|
+
const phoneOps = await service.returnOperations("phone");
|
|
87
|
+
|
|
88
|
+
// Get tag schema
|
|
89
|
+
const tags = await service.getOperationTags("addRoutePartition");
|
|
90
|
+
|
|
91
|
+
// Get detailed metadata (required, nillable, type)
|
|
92
|
+
const detailed = await service.getOperationTagsDetailed("addRoutePartition");
|
|
93
|
+
console.log(detailed.routePartition.required); // true
|
|
94
|
+
console.log(detailed.routePartition.children.name.type); // "string"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Execute Any Operation
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const tags = await service.getOperationTags("addRoutePartition");
|
|
101
|
+
tags.routePartition.name = "INTERNAL-PT";
|
|
102
|
+
tags.routePartition.description = "Internal directory numbers";
|
|
103
|
+
|
|
104
|
+
const result = await service.executeOperation("addRoutePartition", tags);
|
|
105
|
+
console.log("UUID:", result);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Batch Operations
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const results = await service.executeBatch([
|
|
112
|
+
{ operation: "getPhone", tags: { name: "SEP001122334455" } },
|
|
113
|
+
{ operation: "getPhone", tags: { name: "SEP556677889900" } },
|
|
114
|
+
], 5); // concurrency limit
|
|
115
|
+
|
|
116
|
+
results.forEach((r) => {
|
|
117
|
+
console.log(r.success ? `${r.operation}: OK` : `${r.operation}: ${r.error.message}`);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const { AXLAuthError, AXLNotFoundError, AXLOperationError, AXLValidationError } = require("cisco-axl");
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await service.executeOperation("getPhone", { name: "INVALID" });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof AXLAuthError) console.log("Bad credentials");
|
|
130
|
+
else if (error instanceof AXLNotFoundError) console.log("Operation not found:", error.operation);
|
|
131
|
+
else if (error instanceof AXLOperationError) console.log("SOAP fault:", error.message);
|
|
132
|
+
else if (error instanceof AXLValidationError) console.log("Invalid input:", error.message);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Retry Configuration
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
let service = new axlService("10.10.20.1", "admin", "pass", "14.0", {
|
|
140
|
+
retry: {
|
|
141
|
+
retries: 3,
|
|
142
|
+
retryDelay: 1000,
|
|
143
|
+
retryOn: (error) => error.message.includes("ECONNRESET")
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## json-variables Support
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
var lineTemplate = {
|
|
152
|
+
pattern: "%%_extension_%%",
|
|
153
|
+
alertingName: "%%_firstName_%% %%_lastName_%%",
|
|
154
|
+
description: "%%_firstName_%% %%_lastName_%%",
|
|
155
|
+
_data: {
|
|
156
|
+
extension: "1001",
|
|
157
|
+
firstName: "Tom",
|
|
158
|
+
lastName: "Smith",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const lineTags = jVar(lineTemplate);
|
|
163
|
+
await service.executeOperation("updateLine", lineTags);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Methods Reference
|
|
167
|
+
|
|
168
|
+
### Core
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `new axlService(host, user, pass, version, opts?)` | Constructor |
|
|
173
|
+
| `testAuthentication()` | Test credentials against AXL endpoint |
|
|
174
|
+
| `returnOperations(filter?)` | List available operations |
|
|
175
|
+
| `getOperationTags(operation)` | Get tag schema for an operation |
|
|
176
|
+
| `getOperationTagsDetailed(operation)` | Get detailed tag metadata (required/nillable/type) |
|
|
177
|
+
| `executeOperation(operation, tags, opts?)` | Execute any AXL operation |
|
|
178
|
+
| `executeBatch(operations[], concurrency?)` | Parallel batch execution |
|
|
179
|
+
| `setLogLevel(level)` | Change log level at runtime |
|
|
180
|
+
|
|
181
|
+
### Convenience
|
|
182
|
+
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|--------|-------------|
|
|
185
|
+
| `getItem(type, identifier, opts?)` | Get single item by name or UUID |
|
|
186
|
+
| `listItems(type, search?, returnedTags?, opts?)` | List items with filtering |
|
|
187
|
+
| `addItem(type, data, opts?)` | Add a new item |
|
|
188
|
+
| `updateItem(type, identifier, updates, opts?)` | Update an existing item |
|
|
189
|
+
| `removeItem(type, identifier, opts?)` | Remove an item |
|
|
190
|
+
| `executeSqlQuery(sql)` | Run a SQL SELECT query |
|
|
191
|
+
| `executeSqlUpdate(sql)` | Run a SQL INSERT/UPDATE/DELETE |
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Claude Code Hooks for cisco-axl
|
|
2
|
+
|
|
3
|
+
[Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) let you enforce guardrails when AI agents use the CLI. The examples below block write operations so Claude can only read from CUCM.
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
Install the write-safety hook with one command using [cc-hooks-install](https://github.com/sieteunoseis/cc-hooks-install):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx cc-hooks-install add sieteunoseis/cisco-axl
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This fetches the hook definitions from this repo, shows an interactive prompt to select which hooks to install, and merges them into your `~/.claude/settings.json`.
|
|
14
|
+
|
|
15
|
+
If you prefer to install manually, see below.
|
|
16
|
+
|
|
17
|
+
## Block Write Operations
|
|
18
|
+
|
|
19
|
+
Add this to your `~/.claude/settings.json` (global) or `.claude/settings.json` (project-level) under `hooks.PreToolUse`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"hooks": {
|
|
24
|
+
"PreToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Bash",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl (add|update|remove|execute|sql update)'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl write operation. Use --read-only or get explicit user approval.\"}'; fi; }"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### What it blocks
|
|
40
|
+
|
|
41
|
+
| Command | Blocked | Why |
|
|
42
|
+
| ---------------------------------------------- | ------- | ------------------- |
|
|
43
|
+
| `cisco-axl get Phone SEP...` | No | Read operation |
|
|
44
|
+
| `cisco-axl list Phone --search "name=SEP%"` | No | Read operation |
|
|
45
|
+
| `cisco-axl sql query "SELECT ..."` | No | Read-only SQL |
|
|
46
|
+
| `cisco-axl operations --filter phone` | No | Schema discovery |
|
|
47
|
+
| `cisco-axl describe getPhone` | No | Schema discovery |
|
|
48
|
+
| `cisco-axl add Phone --data '{...}'` | **Yes** | Creates a resource |
|
|
49
|
+
| `cisco-axl update Phone SEP... --data '{...}'` | **Yes** | Modifies a resource |
|
|
50
|
+
| `cisco-axl remove Phone SEP...` | **Yes** | Deletes a resource |
|
|
51
|
+
| `cisco-axl execute applyPhone --tags '{...}'` | **Yes** | Executes an action |
|
|
52
|
+
| `cisco-axl sql update "UPDATE ..."` | **Yes** | Modifies database |
|
|
53
|
+
|
|
54
|
+
### Alternative: Use the built-in `--read-only` flag
|
|
55
|
+
|
|
56
|
+
The CLI has a native `--read-only` flag that restricts to `get`, `list`, `describe`, `operations`, and `sql query`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cisco-axl --read-only add Phone --data '{...}'
|
|
60
|
+
# Error: Write operations are not allowed in read-only mode
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can enforce this globally by adding a hook that appends `--read-only` to every command:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"hooks": {
|
|
68
|
+
"PreToolUse": [
|
|
69
|
+
{
|
|
70
|
+
"matcher": "Bash",
|
|
71
|
+
"hooks": [
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl ' && ! echo \"$cmd\" | grep -q '\\-\\-read-only'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl must be run with --read-only. Retry with the flag.\"}'; fi; }"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Audit Logging
|
|
84
|
+
|
|
85
|
+
All cisco-axl operations are logged to `~/.cisco-axl/audit.jsonl` by default. This provides a record of every command run by Claude or any other agent.
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# CLI Reference
|
|
2
|
+
|
|
3
|
+
The CLI provides full AXL access from the command line — CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and a raw execute escape hatch for any AXL operation.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Description |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| `config add/use/list/show/remove/test` | Manage multi-cluster configurations |
|
|
10
|
+
| `get <type> <identifier>` | Get a single item |
|
|
11
|
+
| `list <type>` | List items with search, pagination, returned tags |
|
|
12
|
+
| `add <type>` | Add an item (inline JSON, template, or bulk CSV) |
|
|
13
|
+
| `update <type> <identifier>` | Update an item |
|
|
14
|
+
| `remove <type> <identifier>` | Remove an item |
|
|
15
|
+
| `sql query/update` | Execute SQL against CUCM |
|
|
16
|
+
| `execute <operation>` | Run any raw AXL operation |
|
|
17
|
+
| `operations` | List available operations with `--filter` and `--type crud\|action` |
|
|
18
|
+
| `describe <operation>` | Show tag schema with `--detailed` for required/optional/type info |
|
|
19
|
+
| `doctor` | Check AXL connectivity and configuration health |
|
|
20
|
+
|
|
21
|
+
## Operation Discovery
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Discover available operations
|
|
25
|
+
cisco-axl operations --filter phone
|
|
26
|
+
cisco-axl operations --type action --filter phone
|
|
27
|
+
|
|
28
|
+
# Describe what tags an operation needs
|
|
29
|
+
cisco-axl describe getPhone --detailed
|
|
30
|
+
|
|
31
|
+
# Execute any AXL operation
|
|
32
|
+
cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Bulk Operations from CSV
|
|
36
|
+
|
|
37
|
+
Requires optional packages: `npm install json-variables csv-parse`
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Bulk add phones from template + CSV
|
|
41
|
+
cisco-axl add Phone --template phone-template.json --csv phones.csv
|
|
42
|
+
cisco-axl add Phone --template phone-template.json --csv phones.csv --dry-run # preview first
|
|
43
|
+
|
|
44
|
+
# Single template with inline vars
|
|
45
|
+
cisco-axl add Phone --template phone-template.json --vars '{"mac":"001122334455","dp":"DP_HQ"}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Template file (`phone-template.json`):
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"name": "SEP%%mac%%",
|
|
52
|
+
"devicePoolName": "%%devicePool%%",
|
|
53
|
+
"description": "%%description%%",
|
|
54
|
+
"protocol": "SIP"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Command Chaining
|
|
59
|
+
|
|
60
|
+
Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create a partition, CSS, and line in order
|
|
64
|
+
cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
|
|
65
|
+
cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
|
|
66
|
+
cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Piping with --stdin
|
|
70
|
+
|
|
71
|
+
Use `--stdin` to pipe JSON between commands or from other tools like `jq`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Get a phone's config, modify it with jq, update it
|
|
75
|
+
cisco-axl get Phone SEP001122334455 --format json | \
|
|
76
|
+
jq '.description = "Updated via pipe"' | \
|
|
77
|
+
cisco-axl update Phone SEP001122334455 --stdin
|
|
78
|
+
|
|
79
|
+
# Pipe JSON from a file
|
|
80
|
+
cat phone-config.json | cisco-axl add Phone --stdin
|
|
81
|
+
|
|
82
|
+
# Discover tags, fill them in, execute
|
|
83
|
+
cisco-axl describe applyPhone --format json | \
|
|
84
|
+
jq '.name = "SEP001122334455"' | \
|
|
85
|
+
cisco-axl execute applyPhone --stdin
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `--stdin` flag is available on `add`, `update`, and `execute`. It is mutually exclusive with `--data`/`--tags` and `--template`.
|
|
89
|
+
|
|
90
|
+
## Global Flags
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
--format table|json|toon|csv Output format (default: table)
|
|
94
|
+
--insecure Skip TLS certificate verification
|
|
95
|
+
--clean Remove empty/null values from results
|
|
96
|
+
--no-attributes Remove XML attributes from results
|
|
97
|
+
--read-only Restrict to read-only operations
|
|
98
|
+
--no-audit Disable audit logging for this command
|
|
99
|
+
--debug Enable debug logging
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Output Formats
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cisco-axl list Phone --search "name=SEP%" --format table # default, human-readable
|
|
106
|
+
cisco-axl list Phone --search "name=SEP%" --format json # structured JSON
|
|
107
|
+
cisco-axl list Phone --search "name=SEP%" --format toon # token-efficient for AI agents
|
|
108
|
+
cisco-axl list Phone --search "name=SEP%" --format csv # spreadsheet export
|
|
109
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cisco-axl",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "A library and CLI for Cisco CUCM AXL operations",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "main.mjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"cli-table3": "^0.6.5",
|
|
51
51
|
"commander": "^14.0.3",
|
|
52
52
|
"csv-stringify": "^6.7.0",
|
|
53
|
-
"strong-soap": "
|
|
53
|
+
"strong-soap": "5.0.10",
|
|
54
54
|
"update-notifier": "^7.3.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cisco-axl-cli
|
|
3
3
|
description: Use when managing Cisco CUCM via the cisco-axl CLI — phones, lines, route patterns, partitions, calling search spaces, SIP profiles, and any AXL operation. Covers CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and raw AXL execute commands.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: sieteunoseis
|
|
7
|
+
version: "1.0.0"
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
# Cisco AXL CLI
|
|
7
11
|
|
|
8
12
|
A CLI for Cisco Unified Communications Manager (CUCM) Administrative XML (AXL) operations.
|
|
9
13
|
|
|
10
|
-
##
|
|
14
|
+
## Setup
|
|
11
15
|
|
|
12
16
|
The CLI must be available. Either:
|
|
13
17
|
|
|
@@ -22,20 +26,22 @@ npm install -g cisco-axl
|
|
|
22
26
|
If using npx, prefix all commands with `npx`: `npx cisco-axl list Phone ...`
|
|
23
27
|
If installed globally, use directly: `cisco-axl list Phone ...`
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
### Configuration
|
|
26
30
|
|
|
27
|
-
Configure a CUCM cluster:
|
|
31
|
+
Configure a CUCM cluster (interactive prompt for password — never pass credentials on the command line):
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
cisco-axl config add <name> --host <hostname> --username <user> --
|
|
34
|
+
cisco-axl config add <name> --host <hostname> --username <user> --cucm-version <ver> --insecure
|
|
35
|
+
# You will be prompted securely for the password
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
Valid versions: 11.0, 11.5, 12.0, 12.5, 14.0, 15.0. Use `--insecure` for self-signed certificates (common in CUCM).
|
|
34
39
|
|
|
35
|
-
Or use environment variables:
|
|
40
|
+
Or use environment variables (set via your shell profile, a `.env` file, or a secrets manager — never hardcode credentials):
|
|
36
41
|
|
|
37
42
|
```bash
|
|
38
|
-
|
|
43
|
+
# These should be set securely, e.g. via dotenv, vault, or shell profile
|
|
44
|
+
# CUCM_HOST, CUCM_USERNAME, CUCM_PASSWORD, CUCM_VERSION
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
Test the connection:
|
|
@@ -44,7 +50,7 @@ Test the connection:
|
|
|
44
50
|
cisco-axl config test
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
## Common
|
|
53
|
+
## Common Workflows
|
|
48
54
|
|
|
49
55
|
### Get a single item
|
|
50
56
|
|
|
@@ -90,7 +96,7 @@ cisco-axl sql query "SELECT name, description FROM device WHERE name LIKE 'SEP%'
|
|
|
90
96
|
cisco-axl sql update "UPDATE device SET description='test' WHERE name='SEP001122334455'"
|
|
91
97
|
```
|
|
92
98
|
|
|
93
|
-
|
|
99
|
+
### Discovering Operations
|
|
94
100
|
|
|
95
101
|
This is the CLI's most powerful feature. Discover and use ANY AXL operation dynamically — no static command definitions.
|
|
96
102
|
|
|
@@ -117,7 +123,7 @@ cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
|
|
|
117
123
|
cisco-axl execute applyPhone --tags '{"name":"SEP001122334455"}'
|
|
118
124
|
```
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
### Bulk Operations from CSV
|
|
121
127
|
|
|
122
128
|
For provisioning multiple items, use templates with CSV files. Requires optional packages: `npm install json-variables csv-parse`
|
|
123
129
|
|
|
@@ -173,16 +179,17 @@ Use `--format` to control output:
|
|
|
173
179
|
|
|
174
180
|
**For AI agents:** Use `--format toon` for list queries to reduce token usage. Use `--format json` when you need to parse nested structures.
|
|
175
181
|
|
|
176
|
-
|
|
182
|
+
### Multiple Clusters
|
|
177
183
|
|
|
178
184
|
```bash
|
|
179
|
-
cisco-axl config add lab --host 10.0.0.1 --username admin --
|
|
180
|
-
cisco-axl config add prod --host 10.0.0.2 --username axladmin --
|
|
185
|
+
cisco-axl config add lab --host 10.0.0.1 --username admin --cucm-version 14.0 --insecure
|
|
186
|
+
cisco-axl config add prod --host 10.0.0.2 --username axladmin --cucm-version 15.0 --insecure
|
|
187
|
+
# You will be prompted securely for each password
|
|
181
188
|
cisco-axl config use prod
|
|
182
189
|
cisco-axl list Phone --search "name=SEP%" --cluster lab # override per-command
|
|
183
190
|
```
|
|
184
191
|
|
|
185
|
-
|
|
192
|
+
### Command Chaining
|
|
186
193
|
|
|
187
194
|
Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
|
|
188
195
|
|
|
@@ -193,7 +200,7 @@ cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePart
|
|
|
193
200
|
cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
|
|
194
201
|
```
|
|
195
202
|
|
|
196
|
-
|
|
203
|
+
### Piping with --stdin
|
|
197
204
|
|
|
198
205
|
Use `--stdin` to pipe JSON between commands or from other tools:
|
|
199
206
|
|
|
@@ -210,7 +217,7 @@ cisco-axl describe applyPhone --format json | jq '.name = "SEP001122334455"' | c
|
|
|
210
217
|
|
|
211
218
|
The `--stdin` flag is available on `add`, `update`, and `execute` commands. It is mutually exclusive with `--data`/`--tags` and `--template`.
|
|
212
219
|
|
|
213
|
-
##
|
|
220
|
+
## Global Flags
|
|
214
221
|
|
|
215
222
|
- Item types are PascalCase matching AXL: `Phone`, `Line`, `RoutePartition`, `Css`, `DevicePool`, `SipTrunk`, `TransPattern`, `RouteGroup`, `RouteList`, etc.
|
|
216
223
|
- Use `cisco-axl operations` to discover exact type names.
|