307 lines
9.7 KiB
Markdown
307 lines
9.7 KiB
Markdown
|
|
# RMM Multi-Site Integration Architecture
|
||
|
|
|
||
|
|
## Problem Statement
|
||
|
|
The current integration with Datto RMM only captures devices from a single site per company, leading to incomplete asset visibility for organizations with multiple locations. Since the PSA (Autotask) is the authoritative source for customers/companies, but RMM can have multiple sites per company, we're missing critical infrastructure data.
|
||
|
|
|
||
|
|
## Current Implementation Limitations
|
||
|
|
|
||
|
|
### Single Site Matching Issue
|
||
|
|
- **Current Behavior**: `getDevicesByCompanyName()` finds the first matching site name and returns only those devices
|
||
|
|
- **Data Loss**: Multi-site companies (branch offices, multiple locations) have devices that aren't tracked
|
||
|
|
- **No Mapping System**: Unlike Auvik (NMS) integration, there's no database table or UI to map multiple RMM sites to a single PSA company
|
||
|
|
|
||
|
|
## Recommended Solution Architecture
|
||
|
|
|
||
|
|
### Option 1: Fuzzy Name Matching (Quick Fix)
|
||
|
|
**Description**: Modify the RMM client to find ALL sites that could belong to a company using pattern matching.
|
||
|
|
|
||
|
|
**Implementation**:
|
||
|
|
```typescript
|
||
|
|
async getDevicesByCompanyName(companyName: string): Promise<DattoRMMDevice[]> {
|
||
|
|
const sites = await this.getSites();
|
||
|
|
|
||
|
|
// Find all sites that might belong to this company
|
||
|
|
const matchingSites = sites.filter(site => {
|
||
|
|
const siteName = site.name.toLowerCase();
|
||
|
|
const company = companyName.toLowerCase();
|
||
|
|
|
||
|
|
return siteName.includes(company) ||
|
||
|
|
siteName.startsWith(company) ||
|
||
|
|
// Handle patterns like "Company - Location"
|
||
|
|
siteName.split('-')[0].trim() === company;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get devices from all matching sites
|
||
|
|
const allDevices = await Promise.all(
|
||
|
|
matchingSites.map(site => this.getDevicesBySite(site.uid))
|
||
|
|
);
|
||
|
|
|
||
|
|
return allDevices.flat();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pros**:
|
||
|
|
- Quick to implement
|
||
|
|
- Catches obvious naming patterns
|
||
|
|
- No database changes required
|
||
|
|
|
||
|
|
**Cons**:
|
||
|
|
- Prone to false positives/negatives
|
||
|
|
- Not maintainable long-term
|
||
|
|
- No audit trail
|
||
|
|
- Can't handle complex naming schemes
|
||
|
|
|
||
|
|
### Option 2: Site Mapping Table (RECOMMENDED)
|
||
|
|
**Description**: Create a proper many-to-one mapping between RMM sites and PSA companies, similar to the Auvik tenant mapping system.
|
||
|
|
|
||
|
|
**Database Schema**:
|
||
|
|
```sql
|
||
|
|
CREATE TABLE rmm_site_mappings (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||
|
|
rmm_site_uid VARCHAR(255) NOT NULL,
|
||
|
|
rmm_site_name VARCHAR(255) NOT NULL,
|
||
|
|
is_primary BOOLEAN DEFAULT false,
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
created_by VARCHAR(255),
|
||
|
|
UNIQUE(company_id, rmm_site_uid)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_rmm_site_mappings_company ON rmm_site_mappings(company_id);
|
||
|
|
CREATE INDEX idx_rmm_site_mappings_site_uid ON rmm_site_mappings(rmm_site_uid);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Key Features**:
|
||
|
|
- Multiple sites per company
|
||
|
|
- Primary site designation
|
||
|
|
- Audit trail with timestamps
|
||
|
|
- Notes field for documentation
|
||
|
|
- Foreign key constraints for data integrity
|
||
|
|
|
||
|
|
**Enhanced Service Method**:
|
||
|
|
```typescript
|
||
|
|
async getDevicesByCompanyId(companyId: number): Promise<DattoRMMDevice[]> {
|
||
|
|
// Get all mapped site UIDs from database
|
||
|
|
const siteMappings = await db.query(
|
||
|
|
'SELECT rmm_site_uid FROM rmm_site_mappings WHERE company_id = $1',
|
||
|
|
[companyId]
|
||
|
|
);
|
||
|
|
|
||
|
|
// Fetch devices from all mapped sites
|
||
|
|
const allDevices = await Promise.all(
|
||
|
|
siteMappings.map(mapping =>
|
||
|
|
this.getDevicesBySite(mapping.rmm_site_uid)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
return allDevices.flat();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Option 3: Smart Auto-Discovery (Advanced Enhancement)
|
||
|
|
**Description**: Combine manual mapping with intelligent discovery and pattern recognition.
|
||
|
|
|
||
|
|
**Features**:
|
||
|
|
1. **Pattern Recognition**: Analyze existing site names to detect patterns
|
||
|
|
2. **Company Hierarchy**: Support parent/child company relationships
|
||
|
|
3. **Auto-Suggestions**: When new sites appear, suggest likely company matches
|
||
|
|
4. **Custom Rules**: Allow regex or pattern-based rules for automatic assignment
|
||
|
|
5. **Machine Learning**: Use historical mapping decisions to improve suggestions
|
||
|
|
|
||
|
|
**Auto-Discovery Algorithm**:
|
||
|
|
```typescript
|
||
|
|
interface SiteMatchingSuggestion {
|
||
|
|
siteUid: string;
|
||
|
|
siteName: string;
|
||
|
|
suggestedCompanyId: number;
|
||
|
|
confidence: number;
|
||
|
|
matchReason: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function suggestSiteMappings(): Promise<SiteMatchingSuggestion[]> {
|
||
|
|
const unmappedSites = await getUnmappedSites();
|
||
|
|
const companies = await getAllCompanies();
|
||
|
|
const suggestions: SiteMatchingSuggestion[] = [];
|
||
|
|
|
||
|
|
for (const site of unmappedSites) {
|
||
|
|
// Check exact name match
|
||
|
|
const exactMatch = companies.find(c =>
|
||
|
|
c.name.toLowerCase() === site.name.toLowerCase()
|
||
|
|
);
|
||
|
|
|
||
|
|
if (exactMatch) {
|
||
|
|
suggestions.push({
|
||
|
|
siteUid: site.uid,
|
||
|
|
siteName: site.name,
|
||
|
|
suggestedCompanyId: exactMatch.id,
|
||
|
|
confidence: 0.95,
|
||
|
|
matchReason: 'Exact name match'
|
||
|
|
});
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check partial matches and patterns
|
||
|
|
const partialMatches = companies.filter(c => {
|
||
|
|
const companyName = c.name.toLowerCase();
|
||
|
|
const siteName = site.name.toLowerCase();
|
||
|
|
|
||
|
|
return (
|
||
|
|
siteName.includes(companyName) ||
|
||
|
|
companyName.includes(siteName) ||
|
||
|
|
levenshteinDistance(siteName, companyName) < 3
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (partialMatches.length === 1) {
|
||
|
|
suggestions.push({
|
||
|
|
siteUid: site.uid,
|
||
|
|
siteName: site.name,
|
||
|
|
suggestedCompanyId: partialMatches[0].id,
|
||
|
|
confidence: 0.75,
|
||
|
|
matchReason: 'Partial name match'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return suggestions;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Implementation Phases
|
||
|
|
|
||
|
|
### Phase 1: Foundation (Week 1)
|
||
|
|
- [x] Document current limitations and proposed solutions
|
||
|
|
- [ ] Create database migration for `rmm_site_mappings` table
|
||
|
|
- [ ] Build basic API endpoints for CRUD operations
|
||
|
|
- [ ] Implement service layer for multi-site device fetching
|
||
|
|
|
||
|
|
### Phase 2: User Interface (Week 2)
|
||
|
|
- [ ] Create RMM Site Mappings page (similar to Auvik Mappings)
|
||
|
|
- [ ] Add search and filtering capabilities
|
||
|
|
- [ ] Implement bulk mapping operations
|
||
|
|
- [ ] Add export/import functionality for mappings
|
||
|
|
|
||
|
|
### Phase 3: Intelligence (Week 3)
|
||
|
|
- [ ] Implement auto-discovery suggestions
|
||
|
|
- [ ] Add pattern-based matching rules
|
||
|
|
- [ ] Create notification system for new unmapped sites
|
||
|
|
- [ ] Build reporting dashboard for mapping coverage
|
||
|
|
|
||
|
|
### Phase 4: Optimization (Week 4)
|
||
|
|
- [ ] Add caching layer for site mappings
|
||
|
|
- [ ] Implement bulk device sync for multiple sites
|
||
|
|
- [ ] Create monitoring for site changes
|
||
|
|
- [ ] Add API rate limiting and retry logic
|
||
|
|
|
||
|
|
## Additional Considerations
|
||
|
|
|
||
|
|
### Performance Optimization
|
||
|
|
- **Caching**: Store site mappings in Redis/memory cache
|
||
|
|
- **Batch Processing**: Fetch devices from multiple sites in parallel
|
||
|
|
- **Pagination**: Handle large device counts with proper pagination
|
||
|
|
- **Rate Limiting**: Respect RMM API limits when fetching from multiple sites
|
||
|
|
|
||
|
|
### Data Integrity
|
||
|
|
- **Validation**: Ensure sites aren't mapped to multiple companies
|
||
|
|
- **Cleanup**: Handle deleted sites and companies gracefully
|
||
|
|
- **Sync Status**: Track last sync time per site
|
||
|
|
- **Error Handling**: Implement retry logic for failed site syncs
|
||
|
|
|
||
|
|
### User Experience
|
||
|
|
- **Bulk Operations**: Allow mapping multiple sites at once
|
||
|
|
- **Import/Export**: Support CSV import for initial setup
|
||
|
|
- **Search**: Implement fuzzy search for sites and companies
|
||
|
|
- **Reporting**: Show coverage metrics and unmapped sites
|
||
|
|
|
||
|
|
### Security & Compliance
|
||
|
|
- **Audit Trail**: Log all mapping changes with user identification
|
||
|
|
- **Permissions**: Implement role-based access for mapping management
|
||
|
|
- **Data Privacy**: Ensure site data doesn't leak between companies
|
||
|
|
- **Backup**: Regular backup of mapping configurations
|
||
|
|
|
||
|
|
## API Endpoint Specifications
|
||
|
|
|
||
|
|
### GET /api/rmm/site-mappings
|
||
|
|
Returns all site mappings with optional filtering
|
||
|
|
|
||
|
|
Query Parameters:
|
||
|
|
- `companyId`: Filter by specific company
|
||
|
|
- `unmapped`: Show only unmapped sites
|
||
|
|
- `search`: Search term for site or company names
|
||
|
|
|
||
|
|
### POST /api/rmm/site-mappings
|
||
|
|
Create or update a site mapping
|
||
|
|
|
||
|
|
Request Body:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"companyId": 123,
|
||
|
|
"rmmSiteUid": "site-uid-123",
|
||
|
|
"rmmSiteName": "Company - Branch Office",
|
||
|
|
"isPrimary": false,
|
||
|
|
"notes": "Main branch location"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### DELETE /api/rmm/site-mappings/:id
|
||
|
|
Remove a site mapping
|
||
|
|
|
||
|
|
### POST /api/rmm/site-mappings/suggestions
|
||
|
|
Get auto-discovery suggestions for unmapped sites
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"suggestions": [
|
||
|
|
{
|
||
|
|
"siteUid": "site-123",
|
||
|
|
"siteName": "Acme Corp - Dallas",
|
||
|
|
"suggestedCompanyId": 456,
|
||
|
|
"suggestedCompanyName": "Acme Corp",
|
||
|
|
"confidence": 0.85,
|
||
|
|
"matchReason": "Partial name match"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### POST /api/rmm/site-mappings/bulk
|
||
|
|
Create multiple mappings at once
|
||
|
|
|
||
|
|
Request Body:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"mappings": [
|
||
|
|
{
|
||
|
|
"companyId": 123,
|
||
|
|
"rmmSiteUid": "site-1",
|
||
|
|
"rmmSiteName": "Site 1"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"companyId": 123,
|
||
|
|
"rmmSiteUid": "site-2",
|
||
|
|
"rmmSiteName": "Site 2"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Success Metrics
|
||
|
|
- **Coverage Rate**: % of RMM sites mapped to companies
|
||
|
|
- **Device Visibility**: Total devices visible after multi-site implementation
|
||
|
|
- **Sync Performance**: Time to sync devices across all sites
|
||
|
|
- **User Adoption**: % of companies with multi-site mappings configured
|
||
|
|
- **Error Rate**: Failed sync attempts per site
|
||
|
|
|
||
|
|
## Migration Strategy
|
||
|
|
1. **Backup Current Data**: Export existing single-site mappings
|
||
|
|
2. **Run Migration**: Create new mapping table structure
|
||
|
|
3. **Import Existing**: Convert current implicit mappings to explicit ones
|
||
|
|
4. **Validate**: Ensure no data loss during migration
|
||
|
|
5. **Monitor**: Track sync performance and error rates
|
||
|
|
|
||
|
|
## Conclusion
|
||
|
|
Implementing Option 2 (Site Mapping Table) with elements of Option 3 (Smart Auto-Discovery) provides the best balance of accuracy, maintainability, and user experience. This approach mirrors the successful Auvik integration pattern while addressing the unique challenges of multi-site RMM environments.
|