Initial commit: BMS skill with template support
This commit is contained in:
176
SKILL.md
Normal file
176
SKILL.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# BMS Skill — Kaseya BMS Ticket Management
|
||||
|
||||
Manage service desk tickets in Kaseya BMS (Business Management Solution) via the BMS API v2.
|
||||
|
||||
## Configuration
|
||||
|
||||
Required environment variables (store in shell profile or a secrets manager):
|
||||
|
||||
```bash
|
||||
export BMS_TENANT="your-tenant-name" # Your BMS tenant/subdomain
|
||||
export BMS_USERNAME="user@example.com" # BMS login username
|
||||
export BMS_PASSWORD="yourpassword" # BMS login password
|
||||
# Or use client credentials (OAuth2):
|
||||
export BMS_CLIENT_ID="your-client-id"
|
||||
export BMS_CLIENT_SECRET="your-client-secret"
|
||||
```
|
||||
|
||||
Tokens are cached automatically at `~/.bms_token.json`.
|
||||
|
||||
## Commands
|
||||
|
||||
All commands route through `scripts/bms.sh`. Run without arguments for usage.
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
bms auth # Authenticate and cache token
|
||||
bms auth --status # Show token status / expiry
|
||||
```
|
||||
|
||||
### Listing Tickets
|
||||
|
||||
```bash
|
||||
bms tickets list # All open tickets (paginated)
|
||||
bms tickets list --status "Open" # Filter by status name
|
||||
bms tickets list --assignee "John Smith" # Filter by assignee name
|
||||
bms tickets list --from 2024-01-01 --to 2024-01-31 # Filter by created date range
|
||||
bms tickets list --priority "High" # Filter by priority
|
||||
bms tickets list --queue "Support" # Filter by queue
|
||||
bms tickets list --account "Acme Corp" # Filter by account
|
||||
bms tickets list --page 2 --page-size 50 # Pagination
|
||||
bms tickets list --format json # Raw JSON output
|
||||
```
|
||||
|
||||
### Getting a Ticket
|
||||
|
||||
```bash
|
||||
bms tickets get <ticketId> # Get full ticket details
|
||||
bms tickets get <ticketId> --json # Raw JSON
|
||||
```
|
||||
|
||||
### Creating Tickets
|
||||
|
||||
```bash
|
||||
bms tickets create \
|
||||
--title "Server is down" \
|
||||
--details "The main server stopped responding at 2pm" \
|
||||
--account-id 123 \
|
||||
--location-id 456 \
|
||||
--status-id 1 \
|
||||
--priority-id 2 \
|
||||
--type-id 1 \
|
||||
--source-id 1 \
|
||||
--assignee-id 789
|
||||
```
|
||||
|
||||
Create from a **template** (pre-fills fields; CLI overrides take precedence):
|
||||
|
||||
```bash
|
||||
bms tickets create --template-id 7 --account-id 123 --location-id 456
|
||||
# Fields from template 7 are used; only account/location are overridden.
|
||||
# Any required field still missing triggers an interactive prompt.
|
||||
```
|
||||
|
||||
Or use fully interactive mode (prompts for all required fields):
|
||||
|
||||
```bash
|
||||
bms tickets create --interactive
|
||||
```
|
||||
|
||||
### Updating Tickets
|
||||
|
||||
```bash
|
||||
bms tickets update <ticketId> --status-id 3 # Change status
|
||||
bms tickets update <ticketId> --assignee-id 789 # Reassign
|
||||
bms tickets update <ticketId> --priority-id 1 # Change priority
|
||||
bms tickets update <ticketId> --title "New title" # Update title
|
||||
```
|
||||
|
||||
### Adding Notes
|
||||
|
||||
```bash
|
||||
bms tickets note <ticketId> --message "Called client, investigating"
|
||||
bms tickets note <ticketId> --message "Internal update" --internal
|
||||
bms tickets note <ticketId> --message "Resolved via restart" --status-id 5
|
||||
```
|
||||
|
||||
### Assigning Tickets
|
||||
|
||||
```bash
|
||||
bms tickets assign <ticketId> --assignee-id 789 --note "Routing to tier 2"
|
||||
bms tickets assign <ticketId> --queue-id 3
|
||||
```
|
||||
|
||||
### Resolving Tickets
|
||||
|
||||
```bash
|
||||
bms tickets resolve <ticketId> --comment "Replaced failed drive, server is back online"
|
||||
bms tickets resolve <ticketId> --comment "Fixed" --status-id 6 --publish-kb
|
||||
```
|
||||
|
||||
### Deleting Tickets
|
||||
|
||||
```bash
|
||||
bms tickets delete <ticketId> # Delete single ticket
|
||||
bms tickets delete 123 456 789 # Delete multiple tickets
|
||||
```
|
||||
|
||||
### Lookup Tables (for getting valid IDs)
|
||||
|
||||
```bash
|
||||
bms lookup statuses # List all ticket statuses with IDs
|
||||
bms lookup priorities # List all priorities with IDs
|
||||
bms lookup queues # List all queues with IDs
|
||||
bms lookup issue-types # List all issue types
|
||||
bms lookup assignees # List all assignees/technicians
|
||||
bms lookup ticket-types # List ticket types
|
||||
bms lookup sources # List ticket sources
|
||||
```
|
||||
|
||||
### Templates
|
||||
|
||||
Browse pre-defined ticket, note, and timelog templates configured in BMS.
|
||||
|
||||
```bash
|
||||
bms templates tickets list # List all ticket templates (Id, Name, QueueId, PriorityId, etc.)
|
||||
bms templates tickets get <id> # Inspect a specific ticket template (raw JSON)
|
||||
bms templates notes list # List all note templates
|
||||
bms templates timelogs list # List all timelog templates
|
||||
|
||||
# Add --format json to any list command for raw JSON output
|
||||
bms templates tickets list --format json
|
||||
```
|
||||
|
||||
## Template-Based Ticket Creation
|
||||
|
||||
`bms tickets create --template-id <N>` does the following:
|
||||
|
||||
1. Fetches `GET /v2/servicedesk/templates/tickets/{templateId}` to retrieve template defaults.
|
||||
2. Merges them with any CLI overrides you provide (`--title`, `--description`, `--account-id`, `--location-id`, `--status-id`, `--priority-id`, `--type-id`, `--source-id`, `--assignee-id`, `--queue-id`, `--due-date`, `--contact-id`). CLI values always win.
|
||||
3. Prompts interactively (via stdin) for any required field still missing after the merge.
|
||||
4. Posts the final payload to `POST /v2/servicedesk/tickets`.
|
||||
|
||||
Use `bms templates tickets list` to see available template IDs before creating.
|
||||
|
||||
## Notes / Quirks
|
||||
|
||||
- **Auth**: BMS uses JWT Bearer tokens obtained via `POST /v2/security/authenticate` with `GrantType=password`. Tokens expire; the skill auto-refreshes using the refresh token endpoint.
|
||||
- **Required fields for ticket creation**: Title, Details, AccountId, LocationId, StatusId, PriorityId, TypeId, SourceId, OpenDate — all are required by the API schema.
|
||||
- **IDs not names**: The API uses integer IDs for status, priority, type, etc. Use `bms lookup` commands to find the right IDs for your tenant.
|
||||
- **Search vs GET list**: For filtered searches, `POST /v2/servicedesk/tickets/search` (with body) is more flexible than `GET /v2/servicedesk/tickets` (with query params); this skill uses the POST search by default.
|
||||
- **Pagination**: Default page size is 25. Use `--page-size` (max appears to be 100) and `--page` for large result sets.
|
||||
- **Date format**: Dates should be ISO 8601 strings, e.g. `2024-01-15T00:00:00`.
|
||||
- **Note TypeId**: Required when posting notes. Use `bms lookup note-types` to find valid IDs (typically: 1=Comment, 2=Resolution, etc. — varies by tenant).
|
||||
- **Rate limits**: Not documented in the Swagger spec. Treat as standard REST API — avoid tight loops; add a small sleep between bulk operations.
|
||||
|
||||
## References
|
||||
|
||||
- [BMS API Swagger UI](https://api.bms.kaseya.com/swagger/index.html)
|
||||
- [BMS API Swagger JSON](https://api.bms.kaseya.com/swagger/v2/swagger.json)
|
||||
- `references/key-schemas.md` — TicketInputDto, filter, and note schemas
|
||||
- `scripts/bms.sh` — Main CLI entrypoint
|
||||
- `scripts/bms-auth.sh` — Auth and token management
|
||||
- `scripts/bms-tickets.sh` — Ticket CRUD operations (includes `--template-id` support)
|
||||
- `scripts/bms-lookup.sh` — Lookup table helpers
|
||||
- `scripts/bms-templates.sh` — Template listing commands (tickets, notes, timelogs)
|
||||
107
references/key-schemas.md
Normal file
107
references/key-schemas.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Key BMS API Schemas
|
||||
|
||||
Source: `GET https://api.bms.kaseya.com/swagger/v2/swagger.json`
|
||||
|
||||
## TicketInputDto (POST/PUT /v2/servicedesk/tickets)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| Title | string | ✅ | Ticket subject |
|
||||
| Details | string | ✅ | Body/description |
|
||||
| AccountId | integer | ✅ | Client account ID |
|
||||
| LocationId | integer | ✅ | Account location ID |
|
||||
| StatusId | integer | ✅ | Use `bms lookup statuses` |
|
||||
| PriorityId | integer | ✅ | Use `bms lookup priorities` |
|
||||
| TypeId | integer | ✅ | Use `bms lookup ticket-types` |
|
||||
| SourceId | integer | ✅ | Use `bms lookup sources` |
|
||||
| OpenDate | string (ISO 8601) | ✅ | e.g. `2024-01-15T00:00:00` |
|
||||
| ContactId | integer | | Contact person at the account |
|
||||
| AssigneeId | integer | | Technician assigned |
|
||||
| QueueId | integer | | Queue to place ticket in |
|
||||
| IssueTypeId | integer | | |
|
||||
| SubIssueTypeId | integer | | |
|
||||
| DueDate | string | | ISO 8601 |
|
||||
| SLAId | integer | | |
|
||||
| WorkTypeId | integer | | |
|
||||
|
||||
## TicketFilterDto (inside GetTicketsInputDto.Filter)
|
||||
|
||||
Useful filter fields for `POST /v2/servicedesk/tickets/search`:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| StatusNames | string | Comma-separated or single status name |
|
||||
| AssigneeName | string | Partial match supported |
|
||||
| PriorityNames | string | Priority name(s) |
|
||||
| QueueNames | string | Queue name(s) |
|
||||
| Account | string | Account name search |
|
||||
| Title | string | Ticket title search |
|
||||
| CreatedOnFrom | string (ISO 8601) | Date range start |
|
||||
| CreatedOnTo | string (ISO 8601) | Date range end |
|
||||
| OpenDateFrom / OpenDateTo | string | Open date range |
|
||||
| DueDateFrom / DueDateTo | string | Due date range |
|
||||
| ExcludeCompleted | integer | 1 = exclude completed |
|
||||
| AccountIds | string | Comma-separated account IDs |
|
||||
|
||||
## TicketNotePostInputDto (POST /v2/servicedesk/tickets/{id}/notes)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| Details | string | ✅ | Note body |
|
||||
| IsInternal | boolean | ✅ | true = internal (hidden from client portal) |
|
||||
| TypeId | integer | ✅ | Note type ID — use `bms lookup note-types` |
|
||||
| NoteDate | string (ISO 8601) | ✅ | When the note was created |
|
||||
| StatusId | integer | | Change ticket status simultaneously |
|
||||
|
||||
## AssignTicketInputDto (POST /v2/servicedesk/tickets/{id}/assignticket)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| Details | string | ✅ | Assignment note text |
|
||||
| IsInternal | boolean | ✅ | |
|
||||
| TypeId | integer | ✅ | Note type |
|
||||
| NoteDate | string | ✅ | |
|
||||
| StatusId | integer | ✅ | Status after assignment |
|
||||
| AssigneeId | integer | | New assignee |
|
||||
| QueueId | integer | | Queue to move to |
|
||||
|
||||
## ResolveTicketDto (POST /v2/servicedesk/tickets/{id}/resolve)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| Comment | string | ✅ | Resolution description |
|
||||
| StatusId | integer | | Override resolved status |
|
||||
| IsPublishToKnowledgeBase | boolean | | Add to KB |
|
||||
| IsInternal | boolean | | |
|
||||
|
||||
## Pagination (GetTicketsInputDto)
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| PageNumber | 1-indexed |
|
||||
| PageSize | Max ~100 |
|
||||
| Sort | e.g. `"CreatedOn desc"` |
|
||||
|
||||
## Auth: POST /v2/security/authenticate
|
||||
|
||||
Multipart form-data:
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| GrantType | `"password"` or `"client_credentials"` |
|
||||
| UserName | For password grant |
|
||||
| Password | For password grant |
|
||||
| Tenant | Your tenant name (required) |
|
||||
| ClientId | For client_credentials grant |
|
||||
| ClientSecret | For client_credentials grant |
|
||||
|
||||
Response contains: `AccessToken`, `RefreshToken`, `ExpiresIn`
|
||||
|
||||
## Auth: POST /v2/security/refreshtoken
|
||||
|
||||
JSON body:
|
||||
|
||||
| Field | Required |
|
||||
|---|---|
|
||||
| AccessToken | ✅ |
|
||||
| RefreshToken | ✅ |
|
||||
134
scripts/bms-auth.sh
Executable file
134
scripts/bms-auth.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# bms-auth.sh — Kaseya BMS authentication helper
|
||||
# Obtains and caches JWT tokens. Called by bms.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
|
||||
BMS_TOKEN_FILE="${BMS_TOKEN_FILE:-$HOME/.bms_token.json}"
|
||||
|
||||
# ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
require_env() {
|
||||
local var="$1"
|
||||
[[ -n "${!var:-}" ]] || die "Environment variable $var is required. See SKILL.md for setup."
|
||||
}
|
||||
|
||||
token_is_valid() {
|
||||
# Returns 0 (true) if cached token exists and has not expired (with 60s buffer)
|
||||
[[ -f "$BMS_TOKEN_FILE" ]] || return 1
|
||||
local exp
|
||||
exp=$(jq -r '.expires_at // 0' "$BMS_TOKEN_FILE" 2>/dev/null) || return 1
|
||||
local now
|
||||
now=$(date +%s)
|
||||
[[ $((exp - 60)) -gt $now ]]
|
||||
}
|
||||
|
||||
save_token() {
|
||||
local response="$1"
|
||||
local access_token refresh_token expires_in expires_at
|
||||
access_token=$(echo "$response" | jq -r '.AccessToken // .access_token // empty')
|
||||
refresh_token=$(echo "$response" | jq -r '.RefreshToken // .refresh_token // empty')
|
||||
expires_in=$(echo "$response" | jq -r '.ExpiresIn // .expires_in // 3600')
|
||||
expires_at=$(( $(date +%s) + expires_in ))
|
||||
|
||||
[[ -n "$access_token" ]] || die "No access token in auth response: $response"
|
||||
|
||||
jq -n \
|
||||
--arg at "$access_token" \
|
||||
--arg rt "${refresh_token:-}" \
|
||||
--argjson ea "$expires_at" \
|
||||
'{access_token: $at, refresh_token: $rt, expires_at: $ea}' \
|
||||
> "$BMS_TOKEN_FILE"
|
||||
chmod 600 "$BMS_TOKEN_FILE"
|
||||
}
|
||||
|
||||
# ─── Auth Actions ────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_auth_login() {
|
||||
require_env BMS_TENANT
|
||||
|
||||
local response
|
||||
|
||||
if [[ -n "${BMS_CLIENT_ID:-}" && -n "${BMS_CLIENT_SECRET:-}" ]]; then
|
||||
# OAuth2 client credentials flow
|
||||
echo "Authenticating with client credentials..." >&2
|
||||
response=$(curl -sf -X POST "${BMS_API_BASE}/v2/security/authenticate" \
|
||||
-F "GrantType=client_credentials" \
|
||||
-F "ClientId=${BMS_CLIENT_ID}" \
|
||||
-F "ClientSecret=${BMS_CLIENT_SECRET}" \
|
||||
-F "Tenant=${BMS_TENANT}") || die "Authentication request failed"
|
||||
else
|
||||
# Password flow
|
||||
require_env BMS_USERNAME
|
||||
require_env BMS_PASSWORD
|
||||
echo "Authenticating with username/password..." >&2
|
||||
response=$(curl -sf -X POST "${BMS_API_BASE}/v2/security/authenticate" \
|
||||
-F "GrantType=password" \
|
||||
-F "UserName=${BMS_USERNAME}" \
|
||||
-F "Password=${BMS_PASSWORD}" \
|
||||
-F "Tenant=${BMS_TENANT}") || die "Authentication request failed"
|
||||
fi
|
||||
|
||||
save_token "$response"
|
||||
echo "Authenticated successfully. Token cached at $BMS_TOKEN_FILE" >&2
|
||||
}
|
||||
|
||||
cmd_auth_refresh() {
|
||||
[[ -f "$BMS_TOKEN_FILE" ]] || die "No cached token. Run: bms auth"
|
||||
local access_token refresh_token
|
||||
access_token=$(jq -r '.access_token' "$BMS_TOKEN_FILE")
|
||||
refresh_token=$(jq -r '.refresh_token // empty' "$BMS_TOKEN_FILE")
|
||||
|
||||
[[ -n "$refresh_token" ]] || { cmd_auth_login; return; }
|
||||
|
||||
local response
|
||||
response=$(curl -sf -X POST "${BMS_API_BASE}/v2/security/refreshtoken" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"AccessToken\":\"${access_token}\",\"RefreshToken\":\"${refresh_token}\"}") \
|
||||
|| { echo "Refresh failed, re-authenticating..." >&2; cmd_auth_login; return; }
|
||||
|
||||
save_token "$response"
|
||||
echo "Token refreshed." >&2
|
||||
}
|
||||
|
||||
cmd_auth_status() {
|
||||
if [[ ! -f "$BMS_TOKEN_FILE" ]]; then
|
||||
echo "No token cached."
|
||||
return
|
||||
fi
|
||||
local expires_at now remaining
|
||||
expires_at=$(jq -r '.expires_at // 0' "$BMS_TOKEN_FILE")
|
||||
now=$(date +%s)
|
||||
remaining=$((expires_at - now))
|
||||
if [[ $remaining -gt 0 ]]; then
|
||||
echo "Token valid. Expires in ${remaining}s (at $(date -d "@${expires_at}" 2>/dev/null || date -r "${expires_at}" 2>/dev/null || echo "unknown"))"
|
||||
else
|
||||
echo "Token expired ${remaining#-}s ago."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Public: get_token ───────────────────────────────────────────────────────
|
||||
# Prints the current access token, refreshing/authenticating as needed.
|
||||
get_token() {
|
||||
if ! token_is_valid; then
|
||||
if [[ -f "$BMS_TOKEN_FILE" ]]; then
|
||||
cmd_auth_refresh
|
||||
else
|
||||
cmd_auth_login
|
||||
fi
|
||||
fi
|
||||
jq -r '.access_token' "$BMS_TOKEN_FILE"
|
||||
}
|
||||
|
||||
# ─── Dispatch ────────────────────────────────────────────────────────────────
|
||||
|
||||
case "${1:-}" in
|
||||
login|"") cmd_auth_login ;;
|
||||
refresh) cmd_auth_refresh ;;
|
||||
status) cmd_auth_status ;;
|
||||
get-token) get_token ;;
|
||||
*) echo "Usage: bms-auth.sh [login|refresh|status|get-token]" >&2; exit 1 ;;
|
||||
esac
|
||||
99
scripts/bms-lookup.sh
Executable file
99
scripts/bms-lookup.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# bms-lookup.sh — Fetch lookup tables (statuses, priorities, queues, etc.)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
get_token() {
|
||||
bash "${SCRIPT_DIR}/bms-auth.sh" get-token
|
||||
}
|
||||
|
||||
bms_curl() {
|
||||
local path="$1"; shift
|
||||
local token
|
||||
token=$(get_token)
|
||||
curl -sf -X GET \
|
||||
"${BMS_API_BASE}${path}" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Accept: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
format_lookup() {
|
||||
jq -r '
|
||||
(.Data // .Items // .) |
|
||||
if type == "array" then .[]
|
||||
else .
|
||||
end |
|
||||
"\(.Id // .id)\t\(.Name // .name // .Text // .text // "(unnamed)")"
|
||||
' | sort -n | column -t -s $'\t'
|
||||
}
|
||||
|
||||
cmd_lookup() {
|
||||
local table="$1"
|
||||
case "$table" in
|
||||
statuses|status)
|
||||
echo "=== Ticket Statuses ===" >&2
|
||||
bms_curl "/v2/system/statuses/lookup" | format_lookup
|
||||
;;
|
||||
priorities|priority)
|
||||
echo "=== Priorities ===" >&2
|
||||
bms_curl "/v2/system/priorities/lookup" | format_lookup
|
||||
;;
|
||||
queues|queue)
|
||||
echo "=== Queues ===" >&2
|
||||
bms_curl "/v2/system/queues/lookup" | format_lookup
|
||||
;;
|
||||
issue-types|issuetypes)
|
||||
echo "=== Issue Types ===" >&2
|
||||
bms_curl "/v2/system/issuetypes/lookup" | format_lookup
|
||||
;;
|
||||
sources|source)
|
||||
echo "=== Ticket Sources ===" >&2
|
||||
bms_curl "/v2/system/lookup/TicketSource" | format_lookup
|
||||
;;
|
||||
ticket-types|tickettypes)
|
||||
echo "=== Ticket Types ===" >&2
|
||||
bms_curl "/v2/system/lookup/TicketType" | format_lookup
|
||||
;;
|
||||
assignees|assignee|technicians)
|
||||
echo "=== Assignees / Technicians ===" >&2
|
||||
bms_curl "/v2/hr/assignees/lookup" | format_lookup
|
||||
;;
|
||||
slas|sla)
|
||||
echo "=== SLAs ===" >&2
|
||||
bms_curl "/v2/system/slas/lookup" | format_lookup
|
||||
;;
|
||||
work-types|worktypes)
|
||||
echo "=== Work Types ===" >&2
|
||||
bms_curl "/v2/system/worktypes/lookup" | format_lookup
|
||||
;;
|
||||
note-types|notetypes)
|
||||
echo "=== Note Types (via tenantlookup) ===" >&2
|
||||
bms_curl "/v2/system/tenantlookup/NoteType" | format_lookup
|
||||
;;
|
||||
all)
|
||||
cmd_lookup statuses
|
||||
echo
|
||||
cmd_lookup priorities
|
||||
echo
|
||||
cmd_lookup queues
|
||||
echo
|
||||
cmd_lookup issue-types
|
||||
echo
|
||||
cmd_lookup assignees
|
||||
;;
|
||||
*)
|
||||
die "Unknown lookup table: $table
|
||||
Available: statuses, priorities, queues, issue-types, sources, ticket-types, assignees, slas, work-types, note-types, all"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
table="${1:-}"
|
||||
[[ -n "$table" ]] || die "Usage: bms lookup <statuses|priorities|queues|issue-types|sources|ticket-types|assignees|slas|work-types|note-types|all>"
|
||||
cmd_lookup "$table"
|
||||
185
scripts/bms-templates.sh
Normal file
185
scripts/bms-templates.sh
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
# bms-templates.sh — Kaseya BMS template lookups (tickets, notes, timelogs)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
|
||||
|
||||
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
get_token() {
|
||||
bash "${SCRIPT_DIR}/bms-auth.sh" get-token
|
||||
}
|
||||
|
||||
bms_curl() {
|
||||
local method="$1"; shift
|
||||
local path="$1"; shift
|
||||
local token
|
||||
token=$(get_token)
|
||||
curl -sf -X "$method" \
|
||||
"${BMS_API_BASE}${path}" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# ─── Formatters ──────────────────────────────────────────────────────────────
|
||||
|
||||
format_template_list() {
|
||||
jq -r '
|
||||
(.Data // .Items // .) |
|
||||
if type == "array" then .[] else . end |
|
||||
[
|
||||
(.Id // "-"),
|
||||
(.Name // "(unnamed)"),
|
||||
("Q:" + ((.QueueId // "-") | tostring)),
|
||||
("Pri:" + ((.PriorityId // "-") | tostring)),
|
||||
("Type:" + ((.IssueTypeId // .TypeId // "-") | tostring))
|
||||
] | @tsv
|
||||
' | sort -n | column -t -s $'\t'
|
||||
}
|
||||
|
||||
format_simple_lookup() {
|
||||
# Generic: Id + Name for note/timelog templates
|
||||
jq -r '
|
||||
(.Data // .Items // .) |
|
||||
if type == "array" then .[] else . end |
|
||||
"\(.Id // "-")\t\(.Name // .Text // "(unnamed)")"
|
||||
' | sort -n | column -t -s $'\t'
|
||||
}
|
||||
|
||||
# ─── Templates: Tickets ───────────────────────────────────────────────────────
|
||||
|
||||
cmd_templates_tickets_list() {
|
||||
local format="table"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--format|-f) format="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== Ticket Templates ===" >&2
|
||||
local response
|
||||
response=$(bms_curl GET "/v2/servicedesk/templates/tickets/lookup")
|
||||
|
||||
if [[ "$format" == "json" ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
echo "$response" | format_template_list
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_templates_tickets_get() {
|
||||
local template_id="${1:-}"
|
||||
[[ -n "$template_id" ]] || die "Usage: bms templates tickets get <templateId>"
|
||||
|
||||
bms_curl GET "/v2/servicedesk/templates/tickets/${template_id}" | jq .
|
||||
}
|
||||
|
||||
# ─── Templates: Notes ────────────────────────────────────────────────────────
|
||||
|
||||
cmd_templates_notes_list() {
|
||||
local format="table"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--format|-f) format="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== Note Templates ===" >&2
|
||||
local response
|
||||
response=$(bms_curl GET "/v2/servicedesk/templates/notes/lookup")
|
||||
|
||||
if [[ "$format" == "json" ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
echo "$response" | format_simple_lookup
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Templates: Timelogs ─────────────────────────────────────────────────────
|
||||
|
||||
cmd_templates_timelogs_list() {
|
||||
local format="table"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--format|-f) format="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== Timelog Templates ===" >&2
|
||||
local response
|
||||
response=$(bms_curl GET "/v2/servicedesk/templates/timelogs/lookup")
|
||||
|
||||
if [[ "$format" == "json" ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
echo "$response" | format_simple_lookup
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Dispatch ────────────────────────────────────────────────────────────────
|
||||
|
||||
usage_templates() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: bms templates <resource> <subcommand> [options]
|
||||
|
||||
Resources and subcommands:
|
||||
tickets list List all ticket templates
|
||||
tickets get <id> Get full details for a ticket template
|
||||
notes list List all note templates
|
||||
timelogs list List all timelog templates
|
||||
|
||||
Options:
|
||||
--format json Output raw JSON instead of table
|
||||
|
||||
Examples:
|
||||
bms templates tickets list
|
||||
bms templates tickets get 42
|
||||
bms templates notes list
|
||||
bms templates timelogs list
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
resource="${1:-}"
|
||||
[[ -n "$resource" ]] || usage_templates
|
||||
shift
|
||||
|
||||
case "$resource" in
|
||||
tickets|ticket)
|
||||
subcmd="${1:-list}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
case "$subcmd" in
|
||||
list) cmd_templates_tickets_list "$@" ;;
|
||||
get) cmd_templates_tickets_get "$@" ;;
|
||||
*) die "Unknown tickets template subcommand: $subcmd (available: list, get)" ;;
|
||||
esac
|
||||
;;
|
||||
notes|note)
|
||||
subcmd="${1:-list}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
case "$subcmd" in
|
||||
list) cmd_templates_notes_list "$@" ;;
|
||||
*) die "Unknown notes template subcommand: $subcmd (available: list)" ;;
|
||||
esac
|
||||
;;
|
||||
timelogs|timelog)
|
||||
subcmd="${1:-list}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
case "$subcmd" in
|
||||
list) cmd_templates_timelogs_list "$@" ;;
|
||||
*) die "Unknown timelogs template subcommand: $subcmd (available: list)" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
die "Unknown template resource: $resource (available: tickets, notes, timelogs)"
|
||||
;;
|
||||
esac
|
||||
454
scripts/bms-tickets.sh
Normal file
454
scripts/bms-tickets.sh
Normal file
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env bash
|
||||
# bms-tickets.sh — Kaseya BMS ticket CRUD operations
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
|
||||
|
||||
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
get_token() {
|
||||
bash "${SCRIPT_DIR}/bms-auth.sh" get-token
|
||||
}
|
||||
|
||||
bms_curl() {
|
||||
local method="$1"; shift
|
||||
local path="$1"; shift
|
||||
local token
|
||||
token=$(get_token)
|
||||
|
||||
curl -sf -X "$method" \
|
||||
"${BMS_API_BASE}${path}" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# Pretty-print a ticket list
|
||||
format_ticket_list() {
|
||||
jq -r '
|
||||
(.Data // .Items // .) |
|
||||
if type == "array" then .[] else . end |
|
||||
"\(.TicketNumber // .Id)\t[\(.StatusName // "?")] \(.Title)\t| \(.AccountName // "?")\t| Assignee: \(.AssigneeName // "unassigned")\t| Priority: \(.PriorityName // "?")"
|
||||
' | column -t -s $'\t'
|
||||
}
|
||||
|
||||
format_ticket_detail() {
|
||||
jq -r '
|
||||
.Data // . |
|
||||
"Ticket: \(.TicketNumber) (ID: \(.Id))
|
||||
Title: \(.Title)
|
||||
Status: \(.StatusName) (ID: \(.StatusId))
|
||||
Priority: \(.PriorityName) (ID: \(.PriorityId))
|
||||
Account: \(.AccountName) (ID: \(.AccountId))
|
||||
Location: \(.LocationName // "N/A")
|
||||
Contact: \(.ContactName // "N/A")
|
||||
Queue: \(.QueueName // "N/A")
|
||||
Assignee: \(.AssigneeName // "unassigned")
|
||||
Type: \(.TypeName // "N/A")
|
||||
Created: \(.CreatedOn)
|
||||
Modified: \(.ModifiedOn)
|
||||
Due: \(.DueDate // "none")
|
||||
---
|
||||
\(.Details // "(no details)")"
|
||||
'
|
||||
}
|
||||
|
||||
# ─── Commands ────────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_list() {
|
||||
local status="" assignee="" from="" to="" priority="" queue="" account=""
|
||||
local page=1 page_size=25 format="table"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--status) status="$2"; shift 2 ;;
|
||||
--assignee) assignee="$2"; shift 2 ;;
|
||||
--from) from="$2"; shift 2 ;;
|
||||
--to) to="$2"; shift 2 ;;
|
||||
--priority) priority="$2"; shift 2 ;;
|
||||
--queue) queue="$2"; shift 2 ;;
|
||||
--account) account="$2"; shift 2 ;;
|
||||
--page) page="$2"; shift 2 ;;
|
||||
--page-size) page_size="$2"; shift 2 ;;
|
||||
--format) format="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build filter JSON
|
||||
local filter="{"
|
||||
local sep=""
|
||||
[[ -n "$status" ]] && { filter+="${sep}\"StatusNames\":\"${status}\""; sep=","; }
|
||||
[[ -n "$assignee" ]] && { filter+="${sep}\"AssigneeName\":\"${assignee}\""; sep=","; }
|
||||
[[ -n "$from" ]] && { filter+="${sep}\"CreatedOnFrom\":\"${from}T00:00:00\""; sep=","; }
|
||||
[[ -n "$to" ]] && { filter+="${sep}\"CreatedOnTo\":\"${to}T23:59:59\""; sep=","; }
|
||||
[[ -n "$priority" ]] && { filter+="${sep}\"PriorityNames\":\"${priority}\""; sep=","; }
|
||||
[[ -n "$queue" ]] && { filter+="${sep}\"QueueNames\":\"${queue}\""; sep=","; }
|
||||
[[ -n "$account" ]] && { filter+="${sep}\"Account\":\"${account}\""; sep=","; }
|
||||
filter+="}"
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--argjson filter "$filter" \
|
||||
--argjson page "$page" \
|
||||
--argjson page_size "$page_size" \
|
||||
'{Filter: $filter, PageNumber: $page, PageSize: $page_size}')
|
||||
|
||||
local response
|
||||
response=$(bms_curl POST "/v2/servicedesk/tickets/search" -d "$body")
|
||||
|
||||
if [[ "$format" == "json" ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
local total
|
||||
total=$(echo "$response" | jq -r '.TotalCount // .Total // "?"')
|
||||
echo "Tickets (page ${page}, ${page_size} per page, total: ${total}):" >&2
|
||||
echo "$response" | format_ticket_list
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_get() {
|
||||
local ticket_id="${1:-}"
|
||||
local format="${2:-table}"
|
||||
[[ -n "$ticket_id" ]] || die "Usage: bms tickets get <ticketId>"
|
||||
|
||||
local response
|
||||
response=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}")
|
||||
|
||||
if [[ "$format" == "json" ]] || [[ "${2:-}" == "--json" ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
echo "$response" | format_ticket_detail
|
||||
fi
|
||||
}
|
||||
|
||||
# Prompt for a value if empty; first arg is field label, second is current value (by nameref)
|
||||
# Usage: prompt_if_empty "Label" varname
|
||||
prompt_if_empty() {
|
||||
local label="$1"
|
||||
local -n _ref="$2"
|
||||
if [[ -z "${_ref:-}" ]]; then
|
||||
read -r -p "${label}: " _ref
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_create() {
|
||||
local title="" details="" account_id="" location_id="" contact_id=""
|
||||
local status_id="" priority_id="" type_id="" source_id=""
|
||||
local assignee_id="" queue_id="" due_date="" open_date=""
|
||||
local template_id=""
|
||||
local interactive=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template-id) template_id="$2"; shift 2 ;;
|
||||
--title) title="$2"; shift 2 ;;
|
||||
--details|--description) details="$2"; shift 2 ;;
|
||||
--account-id) account_id="$2"; shift 2 ;;
|
||||
--location-id) location_id="$2"; shift 2 ;;
|
||||
--contact-id) contact_id="$2"; shift 2 ;;
|
||||
--status-id) status_id="$2"; shift 2 ;;
|
||||
--priority-id) priority_id="$2"; shift 2 ;;
|
||||
--type-id) type_id="$2"; shift 2 ;;
|
||||
--source-id) source_id="$2"; shift 2 ;;
|
||||
--assignee-id) assignee_id="$2"; shift 2 ;;
|
||||
--queue-id) queue_id="$2"; shift 2 ;;
|
||||
--due-date) due_date="$2"; shift 2 ;;
|
||||
--interactive) interactive=true; shift ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Template pre-fill ────────────────────────────────────────────────────
|
||||
if [[ -n "$template_id" ]]; then
|
||||
echo "Fetching template ${template_id}..." >&2
|
||||
local tmpl
|
||||
tmpl=$(bms_curl GET "/v2/servicedesk/templates/tickets/${template_id}" | jq '.Data // .')
|
||||
|
||||
# Fill only fields not already set by CLI flags
|
||||
[[ -z "$title" ]] && title=$(echo "$tmpl" | jq -r '.Title // empty')
|
||||
[[ -z "$details" ]] && details=$(echo "$tmpl" | jq -r '.Details // empty')
|
||||
[[ -z "$status_id" ]] && status_id=$(echo "$tmpl" | jq -r '.StatusId // empty')
|
||||
[[ -z "$priority_id" ]] && priority_id=$(echo "$tmpl" | jq -r '.PriorityId // empty')
|
||||
[[ -z "$type_id" ]] && type_id=$(echo "$tmpl" | jq -r '(.TypeId // .IssueTypeId) // empty')
|
||||
[[ -z "$source_id" ]] && source_id=$(echo "$tmpl" | jq -r '.SourceId // empty')
|
||||
[[ -z "$queue_id" ]] && queue_id=$(echo "$tmpl" | jq -r '.QueueId // empty')
|
||||
[[ -z "$assignee_id" ]] && assignee_id=$(echo "$tmpl" | jq -r '.AssigneeId // empty')
|
||||
[[ -z "$account_id" ]] && account_id=$(echo "$tmpl" | jq -r '.AccountId // empty')
|
||||
[[ -z "$location_id" ]] && location_id=$(echo "$tmpl" | jq -r '.LocationId // empty')
|
||||
[[ -z "$contact_id" ]] && contact_id=$(echo "$tmpl" | jq -r '.ContactId // empty')
|
||||
fi
|
||||
|
||||
# ── Interactive prompts ───────────────────────────────────────────────────
|
||||
if $interactive; then
|
||||
prompt_if_empty "Title" title
|
||||
prompt_if_empty "Details" details
|
||||
prompt_if_empty "Account ID" account_id
|
||||
prompt_if_empty "Location ID" location_id
|
||||
prompt_if_empty "Status ID" status_id
|
||||
prompt_if_empty "Priority ID" priority_id
|
||||
prompt_if_empty "Type ID" type_id
|
||||
prompt_if_empty "Source ID" source_id
|
||||
prompt_if_empty "Assignee ID (optional, Enter to skip)" assignee_id
|
||||
else
|
||||
# When using a template, prompt only for fields still missing that are required
|
||||
if [[ -n "$template_id" ]]; then
|
||||
[[ -n "$title" ]] || { read -r -p "Title: " title; }
|
||||
[[ -n "$details" ]] || { read -r -p "Details: " details; }
|
||||
[[ -n "$account_id" ]] || { read -r -p "Account ID: " account_id; }
|
||||
[[ -n "$location_id" ]] || { read -r -p "Location ID: " location_id; }
|
||||
[[ -n "$status_id" ]] || { read -r -p "Status ID: " status_id; }
|
||||
[[ -n "$priority_id" ]] || { read -r -p "Priority ID: " priority_id; }
|
||||
[[ -n "$type_id" ]] || { read -r -p "Type ID: " type_id; }
|
||||
[[ -n "$source_id" ]] || { read -r -p "Source ID: " source_id; }
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n "$title" ]] || die "--title is required"
|
||||
[[ -n "$details" ]] || die "--details is required"
|
||||
[[ -n "$account_id" ]] || die "--account-id is required"
|
||||
[[ -n "$location_id" ]] || die "--location-id is required"
|
||||
[[ -n "$status_id" ]] || die "--status-id is required"
|
||||
[[ -n "$priority_id" ]] || die "--priority-id is required"
|
||||
[[ -n "$type_id" ]] || die "--type-id is required"
|
||||
[[ -n "$source_id" ]] || die "--source-id is required"
|
||||
|
||||
open_date="${open_date:-$(date -u +%Y-%m-%dT%H:%M:%S)}"
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg title "$title" \
|
||||
--arg details "$details" \
|
||||
--argjson account_id "$account_id" \
|
||||
--argjson location_id "$location_id" \
|
||||
--argjson status_id "$status_id" \
|
||||
--argjson priority_id "$priority_id" \
|
||||
--argjson type_id "$type_id" \
|
||||
--argjson source_id "$source_id" \
|
||||
--arg open_date "$open_date" \
|
||||
'{
|
||||
Title: $title,
|
||||
Details: $details,
|
||||
AccountId: $account_id,
|
||||
LocationId: $location_id,
|
||||
StatusId: $status_id,
|
||||
PriorityId: $priority_id,
|
||||
TypeId: $type_id,
|
||||
SourceId: $source_id,
|
||||
OpenDate: $open_date
|
||||
}')
|
||||
|
||||
# Optionally add non-required fields
|
||||
[[ -n "$contact_id" ]] && body=$(echo "$body" | jq --argjson v "$contact_id" '. + {ContactId: $v}')
|
||||
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
|
||||
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
|
||||
[[ -n "$due_date" ]] && body=$(echo "$body" | jq --arg v "$due_date" '. + {DueDate: $v}')
|
||||
|
||||
local response
|
||||
response=$(bms_curl POST "/v2/servicedesk/tickets" -d "$body")
|
||||
echo "$response" | jq -r '"Created ticket ID: \(.Data.Id // .Id) — \(.Data.TicketNumber // .TicketNumber // "N/A")"'
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
local ticket_id="${1:-}"
|
||||
[[ -n "$ticket_id" ]] || die "Usage: bms tickets update <ticketId> [options]"
|
||||
shift
|
||||
|
||||
# Fetch current ticket first so we can do a full PUT with changes merged
|
||||
local current
|
||||
current=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}" | jq '.Data // .')
|
||||
|
||||
local patch="{}"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--title) patch=$(echo "$patch" | jq --arg v "$2" '. + {Title: $v}'); shift 2 ;;
|
||||
--details) patch=$(echo "$patch" | jq --arg v "$2" '. + {Details: $v}'); shift 2 ;;
|
||||
--status-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {StatusId: $v}'); shift 2 ;;
|
||||
--priority-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {PriorityId: $v}'); shift 2 ;;
|
||||
--assignee-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {AssigneeId: $v}'); shift 2 ;;
|
||||
--queue-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {QueueId: $v}'); shift 2 ;;
|
||||
--due-date) patch=$(echo "$patch" | jq --arg v "$2" '. + {DueDate: $v}'); shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Merge patch onto current (keep required fields from current ticket)
|
||||
local body
|
||||
body=$(echo "$current" | jq \
|
||||
--argjson patch "$patch" \
|
||||
'{
|
||||
Title: .Title,
|
||||
Details: .Details,
|
||||
AccountId: .AccountId,
|
||||
LocationId: .LocationId,
|
||||
StatusId: .StatusId,
|
||||
PriorityId: .PriorityId,
|
||||
TypeId: .TypeId,
|
||||
SourceId: .SourceId,
|
||||
OpenDate: .OpenDate,
|
||||
AssigneeId: .AssigneeId,
|
||||
QueueId: .QueueId,
|
||||
ContactId: .ContactId
|
||||
} * $patch')
|
||||
|
||||
local response
|
||||
response=$(bms_curl PUT "/v2/servicedesk/tickets/${ticket_id}" -d "$body")
|
||||
echo "$response" | jq -r '"Updated ticket \(.Data.Id // .Id // "'"$ticket_id"'")"'
|
||||
}
|
||||
|
||||
cmd_note() {
|
||||
local ticket_id="${1:-}"
|
||||
[[ -n "$ticket_id" ]] || die "Usage: bms tickets note <ticketId> --message <text> [options]"
|
||||
shift
|
||||
|
||||
local message="" is_internal=false type_id=1 status_id="" note_date=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--message|-m) message="$2"; shift 2 ;;
|
||||
--internal) is_internal=true; shift ;;
|
||||
--type-id) type_id="$2"; shift 2 ;;
|
||||
--status-id) status_id="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$message" ]] || die "--message is required"
|
||||
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg details "$message" \
|
||||
--argjson is_internal "$is_internal" \
|
||||
--argjson type_id "$type_id" \
|
||||
--arg note_date "$note_date" \
|
||||
'{
|
||||
Details: $details,
|
||||
IsInternal: $is_internal,
|
||||
TypeId: $type_id,
|
||||
NoteDate: $note_date
|
||||
}')
|
||||
|
||||
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
|
||||
|
||||
local response
|
||||
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/notes" -d "$body")
|
||||
echo "$response" | jq -r '"Note added (ID: \(.Data.Id // .Id // "ok"))"'
|
||||
}
|
||||
|
||||
cmd_assign() {
|
||||
local ticket_id="${1:-}"
|
||||
[[ -n "$ticket_id" ]] || die "Usage: bms tickets assign <ticketId> [options]"
|
||||
shift
|
||||
|
||||
local assignee_id="" queue_id="" note="" is_internal=false type_id=1 status_id=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--assignee-id) assignee_id="$2"; shift 2 ;;
|
||||
--queue-id) queue_id="$2"; shift 2 ;;
|
||||
--note|-n) note="$2"; shift 2 ;;
|
||||
--internal) is_internal=true; shift ;;
|
||||
--type-id) type_id="$2"; shift 2 ;;
|
||||
--status-id) status_id="$2"; shift 2 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$note" ]] || note="Ticket reassigned."
|
||||
local note_date
|
||||
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg details "$note" \
|
||||
--argjson is_internal "$is_internal" \
|
||||
--argjson type_id "$type_id" \
|
||||
--arg note_date "$note_date" \
|
||||
'{
|
||||
Details: $details,
|
||||
IsInternal: $is_internal,
|
||||
TypeId: $type_id,
|
||||
NoteDate: $note_date
|
||||
}')
|
||||
|
||||
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
|
||||
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
|
||||
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
|
||||
|
||||
local response
|
||||
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/assignticket" -d "$body")
|
||||
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") assigned."'
|
||||
}
|
||||
|
||||
cmd_resolve() {
|
||||
local ticket_id="${1:-}"
|
||||
[[ -n "$ticket_id" ]] || die "Usage: bms tickets resolve <ticketId> --comment <text> [options]"
|
||||
shift
|
||||
|
||||
local comment="" status_id="" publish_kb=false is_internal=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--comment|-c) comment="$2"; shift 2 ;;
|
||||
--status-id) status_id="$2"; shift 2 ;;
|
||||
--publish-kb) publish_kb=true; shift ;;
|
||||
--internal) is_internal=true; shift ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$comment" ]] || die "--comment is required"
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg comment "$comment" \
|
||||
--argjson publish_kb "$publish_kb" \
|
||||
--argjson is_internal "$is_internal" \
|
||||
'{Comment: $comment, IsPublishToKnowledgeBase: $publish_kb, IsInternal: $is_internal}')
|
||||
|
||||
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
|
||||
|
||||
local response
|
||||
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/resolve" -d "$body")
|
||||
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") resolved."'
|
||||
}
|
||||
|
||||
cmd_delete() {
|
||||
local ids=("$@")
|
||||
[[ ${#ids[@]} -gt 0 ]] || die "Usage: bms tickets delete <id> [id2 ...]"
|
||||
|
||||
if [[ ${#ids[@]} -eq 1 ]]; then
|
||||
bms_curl DELETE "/v2/servicedesk/tickets/${ids[0]}" >/dev/null
|
||||
echo "Deleted ticket ${ids[0]}"
|
||||
else
|
||||
local body
|
||||
body=$(printf '%s\n' "${ids[@]}" | jq -Rs 'split("\n") | map(select(. != "")) | map(tonumber) | {Ids: .}')
|
||||
bms_curl DELETE "/v2/servicedesk/tickets" -d "$body" >/dev/null
|
||||
echo "Deleted tickets: ${ids[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Dispatch ────────────────────────────────────────────────────────────────
|
||||
|
||||
subcmd="${1:-list}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd_list "$@" ;;
|
||||
get) cmd_get "$@" ;;
|
||||
create) cmd_create "$@" ;;
|
||||
update) cmd_update "$@" ;;
|
||||
note) cmd_note "$@" ;;
|
||||
assign) cmd_assign "$@" ;;
|
||||
resolve) cmd_resolve "$@" ;;
|
||||
delete) cmd_delete "$@" ;;
|
||||
*)
|
||||
echo "Usage: bms tickets <list|get|create|update|note|assign|resolve|delete>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
85
scripts/bms.sh
Normal file
85
scripts/bms.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# bms.sh — Kaseya BMS CLI entrypoint
|
||||
# Usage: bms <command> [subcommand] [options]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
bms — Kaseya BMS CLI
|
||||
|
||||
Usage:
|
||||
bms auth [login|refresh|status] Authenticate / manage tokens
|
||||
bms tickets <subcommand> Manage tickets
|
||||
bms templates <resource> <sub> Browse ticket/note/timelog templates
|
||||
bms lookup <table> Fetch lookup tables (IDs for statuses, etc.)
|
||||
|
||||
Tickets subcommands:
|
||||
list List/search tickets
|
||||
get Get a single ticket
|
||||
create Create a new ticket (supports --template-id)
|
||||
update Update ticket fields
|
||||
note Add a note to a ticket
|
||||
assign Reassign a ticket
|
||||
resolve Resolve a ticket
|
||||
delete Delete ticket(s)
|
||||
|
||||
Templates subcommands:
|
||||
tickets list List all ticket templates
|
||||
tickets get <id> Show full details for a ticket template
|
||||
notes list List all note templates
|
||||
timelogs list List all timelog templates
|
||||
|
||||
Lookup tables:
|
||||
statuses, priorities, queues, issue-types, sources, ticket-types,
|
||||
assignees, slas, work-types, note-types, all
|
||||
|
||||
Examples:
|
||||
bms auth
|
||||
bms tickets list --status "Open" --assignee "Jane Doe"
|
||||
bms tickets list --from 2024-01-01 --to 2024-01-31
|
||||
bms tickets get 12345
|
||||
bms tickets create --title "Server down" --details "..." --account-id 1 \
|
||||
--location-id 1 --status-id 1 --priority-id 2 --type-id 1 --source-id 1
|
||||
bms tickets create --template-id 7 --title "Override title" --account-id 99 --location-id 5
|
||||
bms tickets note 12345 --message "Called client" --internal
|
||||
bms tickets resolve 12345 --comment "Fixed by replacing NIC"
|
||||
bms templates tickets list
|
||||
bms templates tickets get 7
|
||||
bms templates notes list
|
||||
bms templates timelogs list
|
||||
bms lookup statuses
|
||||
bms lookup priorities
|
||||
|
||||
Environment variables (required):
|
||||
BMS_TENANT Your BMS tenant name
|
||||
BMS_USERNAME BMS username (or use BMS_CLIENT_ID)
|
||||
BMS_PASSWORD BMS password (or use BMS_CLIENT_SECRET)
|
||||
|
||||
Optional:
|
||||
BMS_CLIENT_ID OAuth2 client ID
|
||||
BMS_CLIENT_SECRET OAuth2 client secret
|
||||
BMS_API_BASE Override API base URL (default: https://api.bms.kaseya.com)
|
||||
BMS_TOKEN_FILE Token cache path (default: ~/.bms_token.json)
|
||||
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
[[ -n "$cmd" ]] || usage
|
||||
shift
|
||||
|
||||
case "$cmd" in
|
||||
auth) exec bash "${SCRIPT_DIR}/bms-auth.sh" "$@" ;;
|
||||
tickets) exec bash "${SCRIPT_DIR}/bms-tickets.sh" "$@" ;;
|
||||
templates) exec bash "${SCRIPT_DIR}/bms-templates.sh" "$@" ;;
|
||||
lookup) exec bash "${SCRIPT_DIR}/bms-lookup.sh" "$@" ;;
|
||||
help|-h|--help) usage ;;
|
||||
*)
|
||||
echo "Unknown command: $cmd" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user