455 lines
16 KiB
Bash
455 lines
16 KiB
Bash
#!/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
|