From a9bcbc258032f85200a094b4422a231254ad86f1 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Tue, 21 Apr 2026 17:59:37 -0700 Subject: [PATCH] Session log: BirthBiologic Datto-to-SharePoint migration Supply Management migrated (160 files), SPMT launched for 4 remaining folders, Syncro ticket #109277420 opened, SPB license assigned to sysadmin. Script, errors, SP site map, and next steps documented. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/migrate-datto-to-sharepoint.ps1 | 31 ++++- .../session-logs/2026-04-21-session.md | 125 +++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1 b/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1 index c9e9cac..7925ae4 100644 --- a/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1 +++ b/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1 @@ -50,7 +50,8 @@ param( [string]$DattoRoot = "C:\Users\Public\Desktop\Datto Workplace Server Projects", [string]$OnlyFolder = "", [switch]$WhatIf, - [bool]$Resume = $true + [bool]$Resume = $true, + [switch]$DeltaOnly ) $ErrorActionPreference = "Stop" @@ -206,6 +207,10 @@ function Upload-LargeFile { $fileSize = (Get-Item $LocalPath).Length $encoded = Encode-Path $RemotePath + # Delete any existing/partial item first so upload session creation doesn't 409 + $deleteUri = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}" + try { Invoke-Graph -Method DELETE -Uri $deleteUri | Out-Null } catch {} + $sessionUri = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}:/createUploadSession" $sessionBody = '{"item":{"@microsoft.graph.conflictBehavior":"replace"}}' $session = Invoke-Graph -Method POST -Uri $sessionUri -Body $sessionBody @@ -241,6 +246,10 @@ function Upload-LargeFile { } $offset += $read } + } catch { + # Cancel the upload session so outer retries can start a fresh one + try { Invoke-RestMethod -Method DELETE -Uri $uploadUrl -ErrorAction SilentlyContinue } catch {} + throw } finally { $stream.Dispose() Write-Host "" @@ -289,6 +298,24 @@ function Migrate-Folder { $sizeMB = [math]::Round($file.Length / 1MB, 2) Write-Log " [$($done + $skip + $fail + 1)/$total] $relPath ($sizeMB MB)" + if ($DeltaOnly) { + # Check if SharePoint already has this file and it's current + $encoded = Encode-Path $remotePath + $checkUri = "${GRAPH_ROOT}/sites/${SiteId}/drive/root:/${encoded}?select=lastModifiedDateTime,size" + try { + $spItem = Invoke-Graph -Method GET -Uri $checkUri + $spDate = [datetime]$spItem.lastModifiedDateTime + $localDate = $file.LastWriteTimeUtc + if ($spItem.size -gt 0 -and $spDate -ge $localDate) { + $skip++ + continue + } + Write-Log " [DELTA] $relPath (SP: $($spDate.ToString('yyyy-MM-dd')) / Local: $($localDate.ToString('yyyy-MM-dd')))" + } catch { + # Not found in SP — upload it + } + } + if ($WhatIf) { Write-Log " [WHATIF] -> $remotePath" $done++ @@ -325,7 +352,7 @@ function Migrate-Folder { # Main Write-Log "=== BirthBiologic Datto -> SharePoint Migration ===" Write-Log "Source: $DattoRoot" -Write-Log "WhatIf=$WhatIf | Resume=$Resume | OnlyFolder=$(if ($OnlyFolder) { $OnlyFolder } else { '(all)' })" +Write-Log "WhatIf=$WhatIf | Resume=$Resume | DeltaOnly=$DeltaOnly | OnlyFolder=$(if ($OnlyFolder) { $OnlyFolder } else { '(all)' })" if (-not (Test-Path $DattoRoot)) { Write-Log "ERROR: Datto root not found: $DattoRoot" "ERROR" diff --git a/clients/birth-biologic/session-logs/2026-04-21-session.md b/clients/birth-biologic/session-logs/2026-04-21-session.md index e5d7c35..18df5f7 100644 --- a/clients/birth-biologic/session-logs/2026-04-21-session.md +++ b/clients/birth-biologic/session-logs/2026-04-21-session.md @@ -45,6 +45,125 @@ New client onboarded into GuruRMM. Client and site created. Vault entry saved. M - [ ] Install GuruRMM agent on BirthBiologic server via MSI or landing page - [ ] Consent remaining apps in BirthBiologic tenant (user-manager, tenant-admin minimum) -- [ ] Datto Workplace → SharePoint migration: PowerShell script using tenant-admin app-only credentials, reads local Datto file server, uploads to SharePoint via Graph API `Sites.ReadWrite.All` - - BirthBiologic has 14 SharePoint sites (5 new dept sites created 2026-04-20 for Datto migration) - - Datto Workplace server is on-premise at their office (local file system access available once agent is installed) +- [x] Install GuruRMM agent on BB-SERVER — completed, agent online +- [x] Consent tenant-admin app in BirthBiologic tenant for Sites.ReadWrite.All +- [x] Build PowerShell migration script (migrate-datto-to-sharepoint.ps1) +- [x] Supply Management folder — 160/160 files migrated to SharePoint +- [x] Opened Syncro ticket #109277420 for this migration project +- [x] M365 Business Premium license assigned to sysadmin@birthbiologic.com +- [x] SPMT migration launched for Admin, Birth Biologic Activity Reports, Donor Services, Quality Department +- [ ] SPMT migration complete — check morning status +- [ ] After client tests SharePoint access, run delta sync (`-DeltaOnly` flag) for changed files +- [ ] Two duplicate Syncro comments on #109277420 need manual GUI deletion (no API delete for comments) +- [ ] Verify ITSvcs state file entry on BB-SERVER is not causing issues (ITSvcs is ACG-owned, excluded from migration) + +--- + +## Update: 17:58 — Datto-to-SharePoint Migration (Full Detail) + +### What Was Accomplished + +1. **GuruRMM agent installed on BB-SERVER** — agent came online, used as command channel for remote PowerShell execution throughout session. + +2. **Tenant-admin app consented in BirthBiologic tenant** — consent URL used: + `https://login.microsoftonline.com//adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com` + (redirect URI must match app manifest — `https://azcomputerguru.com`, NOT `https://rmm.azcomputerguru.com`) + +3. **Migration script built** — `D:/claudetools/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1` + - TLS 1.2 enforcement at top (`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12`) + - Token auto-refresh, resume via JSON state file + - Small files: PUT to `/content`; large files: chunked upload session + - Pre-delete before upload session to avoid 409 conflicts + - `-DeltaOnly` switch: skips files where SP size > 0 and SP lastModified >= local lastWriteTimeUtc + - `-WhatIf` mode, `-OnlyFolder` for per-folder targeting + - All ASCII characters only (no Unicode box-drawing — causes PS5.1 parse errors) + - Hashtable merge via foreach loop (PS5 doesn't support `@{} + @{}`) + - `${encodedPath}` not `$encodedPath:` in URL strings (PS interprets colon as drive reference) + +4. **Supply Management migrated** — 160/160 files via script. + - 159 transferred via RMM-launched script on BB-SERVER + - 1 file (8 MB PDF) timed out RMM channel (~77 KB/s upload); base64-encoded on BB-SERVER, captured stdout, decoded locally, uploaded directly via Python urllib to Graph API + +5. **SPMT launched** for remaining 4 folders: + - Admin → `https://birthbiologic.sharepoint.com/sites/Admin` + - Birth Biologic Activity Reports → `https://birthbiologic.sharepoint.com/sites/Admin` (same site, Documents root; SPMT preserves source folder name as subfolder) + - Donor Services → `https://birthbiologic.sharepoint.com/sites/DonorServices` + - Quality Department → `https://birthbiologic.sharepoint.com/sites/QualityDepartment` + - ITSvcs excluded — that is ACG's folder, not client data + - 20% progress on Donor Services observed before end of session + - Connection noted as slow but making progress + +6. **Syncro ticket #109277420 created** + - Customer: BirthBiologic + - Subject: Datto Workplace to SharePoint Migration + - Contact: Annise + - Assigned: Mike Swanson (user_id 1735) + - Priority: Normal + - Due: 2026-04-22 + - Comment posted with migration status (use `
` line breaks — `
  • ` collapses in Syncro renderer) + +7. **M365 Business Premium license assigned to sysadmin@birthbiologic.com** + - SKU: M365 Business Premium (cbdc14ab-d96c-4132-b7f4-1f3a3a819bb4) + - SPB includes EMS — EMS standalone license removed + - sysadmin confirmed as SharePoint admin (needed for SPMT destination access) + +### Credentials + +- **Tenant-admin app client ID:** `709e6eed-0711-4875-9c44-2d3518c47063` +- **Tenant-admin app secret:** `D:/vault/msp-tools/computerguru-tenant-admin.sops.yaml` → `credentials.credential` +- **BirthBiologic tenant ID:** Look up via Graph or from previous remediation work +- **GuruRMM JWT secret:** `D:/vault/projects/gururmm/api-server.sops.yaml` → `credentials.credential` +- **GuruRMM agent API key for BB site:** `grmm_1ZB1qV9Q61b9Noq8BIaZGwLNjZMfF49i` +- **Syncro API key:** `D:/vault/msp-tools/syncro.sops.yaml` → `credentials.credential` + +### SharePoint Site Map (BirthBiologic) + +| Datto Folder | SharePoint Site | Site ID | +|---|---|---| +| Admin | birthbiologic.sharepoint.com/sites/Admin | `1baf65c1-...` (see script) | +| Birth Biologic Activity Reports | birthbiologic.sharepoint.com/sites/Admin | same as Admin | +| Donor Services | birthbiologic.sharepoint.com/sites/DonorServices | `bcbfa272-...` (see script) | +| Quality Department | birthbiologic.sharepoint.com/sites/QualityDepartment | `5fd38089-...` (see script) | +| Supply Management | birthbiologic.sharepoint.com/sites/SupplyManagement | `4700ecf3-...` (see script) | +| ITSvcs | EXCLUDED — ACG folder | — | + +Full site IDs are hardcoded in the script (`$SITE_MAP` hashtable). + +### Infrastructure + +- **BB-SERVER:** BirthBiologic on-premise Windows Server 2016, GuruRMM agent installed +- **GuruRMM server:** `https://rmm.azcomputerguru.com` (172.16.3.30:3001) +- **GuruRMM JWT claims required:** `sub`, `role`, `orgs`, `exp`, `iat` — all must be present or 401 +- **GuruRMM command body:** must include `command_type: "powershell"` — missing = 422 + +### Key Errors and Resolutions + +| Error | Cause | Fix | +|---|---|---| +| Wrong consent redirect URI 400 | Used rmm.azcomputerguru.com (not in manifest) | Use https://azcomputerguru.com | +| JWT 401 | Missing role/orgs/iat claims | Include all required claims | +| JWT 422 | Missing command_type field | Add `"command_type": "powershell"` | +| Wrong vault path | projects/gururmm/jwt.sops.yaml doesn't exist | Use projects/gururmm/api-server.sops.yaml | +| PS parse error — Unicode | Box-drawing chars in PS5.1 comments | Rewrite all comments ASCII-only | +| PS parse error — hashtable merge | `@{} + @{}` invalid in PS5 | Use foreach loop | +| PS parse error — drive ref | `$encodedPath:/content` | Use `${encodedPath}:/content` | +| TLS error on BB-SERVER | Win Server 2016 defaults TLS 1.0 | Add `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12` | +| 409 Conflict on retry | Partial file left in SP after failed upload | DELETE item before createUploadSession | +| RMM timeout 8 MB file | ~77 KB/s upload > 300s timeout | Base64 on server, capture stdout, upload locally | +| RMSBASIC not assignable | Service plan, not standalone SKU | Use M365 Business Premium (cbdc14ab) | +| SPB 0 seats | License change not saved in admin center | Polled until seat appeared, then assigned | +| Syncro ul/li collapsed | Syncro renderer collapses block-level list tags | Use `
    ` line breaks instead | + +### Files Created/Modified + +- `D:/claudetools/clients/birth-biologic/scripts/migrate-datto-to-sharepoint.ps1` — full migration script +- `C:/Users/guru/.claude/projects/D--claudetools/memory/feedback_syncro_html.md` — new memory: use `
    ` in Syncro comments + +### Next Steps (Morning) + +1. Check SPMT migration status — all 4 folders should be complete or near-complete +2. Verify file counts in each SharePoint site match Datto source +3. Notify Annise that migration is complete, ask her to test access +4. After client confirms access, schedule delta sync window: run script with `-DeltaOnly` to catch any files changed since initial migration +5. Delete two duplicate/ugly Syncro comments manually in GUI (ticket #109277420) +6. Update Syncro ticket with completion status and bill time