ConnectWise PSA skills and tools under ops/connectwise (hub-sanitized 2026-11-01: client intel, internal workflow/credential/member/company ids, archived-page apiToken/sessionId stripped to placeholders)
This commit is contained in:
commit
9c96092034
36 files changed
+310875
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
import requests, base64, json, datetime, re
|
||||
|
||||
# Generic ConnectWise cert-renewal scheduler.
|
||||
# Usage: edit TICKETS list, run. Schedule = expiry minus 7 days;
|
||||
# Sat/Sun shifts to preceding Friday; 9am PDT start, staggered +1h per same-day block.
|
||||
# Set PATCH_STATUS=True to flip tickets to 'Scheduled' (status id 13) after entry creation.
|
||||
|
||||
def _load_creds():
|
||||
"""Load ConnectWise API creds from ~/.secrets/connectwise or env."""
|
||||
path = os.path.expanduser("~/.secrets/connectwise")
|
||||
keys = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.lstrip().startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
keys[k.strip()] = v.strip()
|
||||
for k in ("CW_COMPANY_ID", "CW_PUBLIC_KEY", "CW_PRIVATE_KEY", "CW_CLIENT_ID"):
|
||||
if os.getenv(k):
|
||||
keys[k] = os.environ[k]
|
||||
missing = [
|
||||
k for k in ("CW_COMPANY_ID", "CW_PUBLIC_KEY", "CW_PRIVATE_KEY", "CW_CLIENT_ID")
|
||||
if not keys.get(k)
|
||||
]
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
f"missing {missing}: export them or populate ~/.secrets/connectwise"
|
||||
)
|
||||
return keys
|
||||
|
||||
|
||||
_CREDS = _load_creds()
|
||||
COMPANY_ID = _CREDS["CW_COMPANY_ID"]
|
||||
PUBLIC_KEY = _CREDS["CW_PUBLIC_KEY"].lstrip("+")
|
||||
PRIVATE_KEY = _CREDS["CW_PRIVATE_KEY"]
|
||||
CLIENT_ID = _CREDS["CW_CLIENT_ID"]
|
||||
|
||||
BASE = "https://cw.vistanetinc.com/v4_6_release/apis/3.0"
|
||||
MEMBER_ID = 147 # SBrown
|
||||
WHERE_ID = 4 # Remote
|
||||
SCHED_STATUS_ID = 2 # Firm
|
||||
SCHED_TYPE_ID = 4 # 'S'
|
||||
SPAN_ID = 1 # normal block
|
||||
REMINDER_ID = 4 # 15 minutes
|
||||
TICKET_STATUS_SCHEDULED = 13
|
||||
PATCH_STATUS = False # True -> also PATCH ticket status to Scheduled
|
||||
|
||||
auth = base64.b64encode(f"{COMPANY_ID}+{PUBLIC_KEY}:{PRIVATE_KEY}".encode()).decode()
|
||||
H = {"Authorization": f"Basic {auth}", "clientId": CLIENT_ID, "Content-Type": "application/json"}
|
||||
|
||||
TICKETS = [] # e.g. [122333, 122334, 122332, 122335, 122331, 122330, 122329, 122328]
|
||||
|
||||
|
||||
def plan_date(expiry):
|
||||
d = datetime.date.fromisoformat(expiry) - datetime.timedelta(days=7)
|
||||
while d.weekday() >= 5:
|
||||
d -= datetime.timedelta(days=1)
|
||||
return d
|
||||
|
||||
|
||||
def main():
|
||||
plan, slot = [], {}
|
||||
for tid in TICKETS:
|
||||
t = requests.get(f"{BASE}/service/tickets/{tid}", headers=H).json()
|
||||
m = re.search(r"Expiring on (\d{4}-\d{2}-\d{2})", t.get("summary") or "")
|
||||
expiry = m.group(1)
|
||||
d = plan_date(expiry)
|
||||
hour = 16 + slot.get(d, 0)
|
||||
slot[d] = slot.get(d, 0) + 1
|
||||
plan.append({"tid": tid, "co": t["company"]["identifier"], "expiry": expiry,
|
||||
"date": d.isoformat(),
|
||||
"start": f"{d.isoformat()}T{hour:02d}:00:00Z",
|
||||
"end": f"{d.isoformat()}T{hour+1:02d}:00:00Z"})
|
||||
for p in plan:
|
||||
r = requests.get(f"{BASE}/schedule/entries", headers=H,
|
||||
params={"conditions": f"(objectId = {p['tid']})", "pageSize": 50})
|
||||
if any(e.get("dateStart") for e in r.json()):
|
||||
print(f"{p['tid']}: SKIP (existing dated block)"); continue
|
||||
body = {"name": f"{p['co']} / Domain certificate renewal expiring {p['expiry']}",
|
||||
"objectId": p["tid"], "member": {"id": MEMBER_ID}, "where": {"id": WHERE_ID},
|
||||
"dateStart": p["start"], "dateEnd": p["end"], "reminder": {"id": REMINDER_ID},
|
||||
"status": {"id": SCHED_STATUS_ID}, "type": {"id": SCHED_TYPE_ID},
|
||||
"span": {"id": SPAN_ID}, "hours": 1.0}
|
||||
r = requests.post(f"{BASE}/schedule/entries", headers=H, json=body)
|
||||
print(f"{p['tid']}: {r.status_code} {r.json().get('id') if r.status_code == 201 else r.text[:150]}")
|
||||
if PATCH_STATUS:
|
||||
requests.patch(f"{BASE}/service/tickets/{p['tid']}", headers=H,
|
||||
json=[{"op": "replace", "path": "status/id", "value": TICKET_STATUS_SCHEDULED}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user