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