wxcli 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- wxcli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Line mapper: CUCM Directory Numbers -> Webex Phone Numbers + Extensions.
|
|
2
|
+
|
|
3
|
+
Maps CUCM DNs (identified from device line appearances) to CanonicalLine objects
|
|
4
|
+
with E.164 normalization. Each unique DN (pattern + partition) becomes one
|
|
5
|
+
CanonicalLine.
|
|
6
|
+
|
|
7
|
+
(from 03b-transform-mappers.md §3, line_mapper)
|
|
8
|
+
|
|
9
|
+
Cross-ref reads:
|
|
10
|
+
device_has_dn (Device -> DN) — reverse-queried to find devices per DN
|
|
11
|
+
dn_in_partition (DN -> Partition)
|
|
12
|
+
device_in_pool (Device -> DevicePool) — for location chain
|
|
13
|
+
device_pool_to_location (DevicePool -> Location) — for country code
|
|
14
|
+
|
|
15
|
+
Does NOT produce: EXTENSION_CONFLICT (analyzer-owned)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from wxcli.migration.models import (
|
|
25
|
+
CanonicalLine,
|
|
26
|
+
DecisionType,
|
|
27
|
+
LineClassification,
|
|
28
|
+
MapperResult,
|
|
29
|
+
MigrationStatus,
|
|
30
|
+
Provenance,
|
|
31
|
+
)
|
|
32
|
+
from wxcli.migration.store import MigrationStore
|
|
33
|
+
from wxcli.migration.transform.e164 import normalize_dn
|
|
34
|
+
from wxcli.migration.transform.mappers.base import (
|
|
35
|
+
Mapper,
|
|
36
|
+
decision_to_store_dict,
|
|
37
|
+
manual_option,
|
|
38
|
+
skip_option,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Webex extension length constraints
|
|
44
|
+
# (from 03b-transform-mappers.md §3: "Extensions are 2-10 chars in Webex")
|
|
45
|
+
_MIN_EXTENSION_LENGTH = 2
|
|
46
|
+
_MAX_EXTENSION_LENGTH = 10
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LineMapper(Mapper):
|
|
50
|
+
"""Maps CUCM Directory Numbers to Webex Phone Numbers and Extensions.
|
|
51
|
+
|
|
52
|
+
(from 03b-transform-mappers.md §3)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name = "line_mapper"
|
|
56
|
+
depends_on = ["location_mapper"]
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
default_country_code: str = "US",
|
|
61
|
+
site_prefix_rules: list[dict] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Args:
|
|
65
|
+
default_country_code: ISO 3166-1 alpha-2 code for E.164 normalization
|
|
66
|
+
(from 03b-transform-mappers.md §3: country code from location)
|
|
67
|
+
site_prefix_rules: List of prefix stripping rules from migration config
|
|
68
|
+
"""
|
|
69
|
+
self.default_country_code = default_country_code
|
|
70
|
+
self.site_prefix_rules = site_prefix_rules or []
|
|
71
|
+
|
|
72
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
73
|
+
"""Find all unique DNs from device line appearances and produce CanonicalLine objects."""
|
|
74
|
+
result = MapperResult()
|
|
75
|
+
|
|
76
|
+
# Collect all unique DN IDs from device_has_dn cross-refs
|
|
77
|
+
all_dn_refs = store.get_cross_refs(relationship="device_has_dn")
|
|
78
|
+
unique_dn_ids: set[str] = set()
|
|
79
|
+
# Track which devices reference each DN (for shared line detection)
|
|
80
|
+
dn_to_devices: dict[str, list[str]] = {}
|
|
81
|
+
for ref in all_dn_refs:
|
|
82
|
+
dn_id = ref["to_id"]
|
|
83
|
+
dev_id = ref["from_id"]
|
|
84
|
+
unique_dn_ids.add(dn_id)
|
|
85
|
+
dn_to_devices.setdefault(dn_id, []).append(dev_id)
|
|
86
|
+
|
|
87
|
+
# Build index of pre-computed e164_results from device line appearances
|
|
88
|
+
# (from Phase 04 corrections: CrossReferenceBuilder computes e164_result
|
|
89
|
+
# on each line appearance — the line_mapper can read this directly)
|
|
90
|
+
precomputed_e164: dict[str, dict[str, Any]] = {}
|
|
91
|
+
self._build_precomputed_e164_index(store, dn_to_devices, precomputed_e164)
|
|
92
|
+
|
|
93
|
+
for dn_id in sorted(unique_dn_ids):
|
|
94
|
+
# Parse DN ID format: "dn:pattern:partition"
|
|
95
|
+
parts = dn_id.split(":", 2)
|
|
96
|
+
if len(parts) < 2:
|
|
97
|
+
continue
|
|
98
|
+
dn_pattern = parts[1]
|
|
99
|
+
partition = parts[2] if len(parts) > 2 else "<None>"
|
|
100
|
+
country_code: str | None = None
|
|
101
|
+
|
|
102
|
+
# Check for pre-computed e164_result from line appearances
|
|
103
|
+
# (from Phase 04 corrections: use e164_result when available)
|
|
104
|
+
cached = precomputed_e164.get(dn_id)
|
|
105
|
+
if cached:
|
|
106
|
+
e164_result_data = cached
|
|
107
|
+
# Build an E164Result-compatible object from the cached dict
|
|
108
|
+
e164_result = _E164ResultProxy(
|
|
109
|
+
e164=e164_result_data.get("e164"),
|
|
110
|
+
extension=e164_result_data.get("extension"),
|
|
111
|
+
raw=dn_pattern,
|
|
112
|
+
classification=e164_result_data.get("classification", "AMBIGUOUS"),
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
# Fallback: resolve country code and run normalize_dn()
|
|
116
|
+
country_code = self._resolve_country_code(store, dn_id, dn_to_devices)
|
|
117
|
+
e164_result = normalize_dn(
|
|
118
|
+
dn_pattern, country_code, self.site_prefix_rules
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Shared line detection
|
|
122
|
+
# (from 03b-transform-mappers.md §3: tag shared:true when multiple devices)
|
|
123
|
+
device_list = dn_to_devices.get(dn_id, [])
|
|
124
|
+
unique_devices = list(set(device_list))
|
|
125
|
+
shared = len(unique_devices) > 1
|
|
126
|
+
|
|
127
|
+
# Map classification string to enum
|
|
128
|
+
classification = _classify(e164_result.classification)
|
|
129
|
+
|
|
130
|
+
# Build extension and e164 fields
|
|
131
|
+
extension = None
|
|
132
|
+
e164 = None
|
|
133
|
+
|
|
134
|
+
if classification == LineClassification.EXTENSION:
|
|
135
|
+
extension = e164_result.extension or dn_pattern
|
|
136
|
+
elif classification in (LineClassification.NATIONAL, LineClassification.E164):
|
|
137
|
+
e164 = e164_result.e164
|
|
138
|
+
# For national/E164 numbers, the raw pattern may also serve as extension
|
|
139
|
+
# if it looks like one. But per the design, extension comes from the
|
|
140
|
+
# classified result — NATIONAL/E164 numbers don't get an extension field
|
|
141
|
+
# unless separately configured. The user_mapper gets bare extension from
|
|
142
|
+
# user_has_primary_dn independently.
|
|
143
|
+
# However: a 10-digit national number IS the extension in many CUCM deployments
|
|
144
|
+
# (from 03b-transform-mappers.md §3: "If classified as EXTENSION: stored as bare extension")
|
|
145
|
+
extension = e164_result.extension
|
|
146
|
+
elif classification == LineClassification.AMBIGUOUS:
|
|
147
|
+
# DN can't be classified
|
|
148
|
+
# (from 03b-transform-mappers.md §3: DN_AMBIGUOUS decision)
|
|
149
|
+
decision = self._create_decision(
|
|
150
|
+
store=store,
|
|
151
|
+
decision_type=DecisionType.DN_AMBIGUOUS,
|
|
152
|
+
severity="MEDIUM",
|
|
153
|
+
summary=f"DN '{dn_pattern}' (partition: {partition}) cannot be classified as extension, national, or E.164",
|
|
154
|
+
context={
|
|
155
|
+
"dn_id": dn_id,
|
|
156
|
+
"dn_pattern": dn_pattern,
|
|
157
|
+
"partition": partition,
|
|
158
|
+
"country_code": country_code,
|
|
159
|
+
},
|
|
160
|
+
options=[
|
|
161
|
+
manual_option("Admin classifies as extension-only"),
|
|
162
|
+
manual_option("Admin provides correct E.164"),
|
|
163
|
+
skip_option("Number not migrated"),
|
|
164
|
+
],
|
|
165
|
+
affected_objects=[dn_id],
|
|
166
|
+
)
|
|
167
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
168
|
+
result.decisions.append(decision)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Validate extension length for Webex
|
|
172
|
+
# (from 03b-transform-mappers.md §3: "extensions outside 2-10 chars produce MISSING_DATA")
|
|
173
|
+
if extension and (
|
|
174
|
+
len(extension) < _MIN_EXTENSION_LENGTH
|
|
175
|
+
or len(extension) > _MAX_EXTENSION_LENGTH
|
|
176
|
+
):
|
|
177
|
+
decision = self._create_decision(
|
|
178
|
+
store=store,
|
|
179
|
+
decision_type=DecisionType.MISSING_DATA,
|
|
180
|
+
severity="MEDIUM",
|
|
181
|
+
summary=f"DN '{dn_pattern}' resolves to extension '{extension}' which is outside Webex's 2-10 char limit",
|
|
182
|
+
context={
|
|
183
|
+
"dn_id": dn_id,
|
|
184
|
+
"dn_pattern": dn_pattern,
|
|
185
|
+
"extension": extension,
|
|
186
|
+
"extension_length": len(extension),
|
|
187
|
+
"partition": partition,
|
|
188
|
+
},
|
|
189
|
+
options=[
|
|
190
|
+
manual_option("Admin provides valid extension (2-10 chars)"),
|
|
191
|
+
skip_option("Number not migrated"),
|
|
192
|
+
],
|
|
193
|
+
affected_objects=[dn_id],
|
|
194
|
+
)
|
|
195
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
196
|
+
result.decisions.append(decision)
|
|
197
|
+
# Still create the line object but flag it
|
|
198
|
+
# (user can resolve the decision later)
|
|
199
|
+
|
|
200
|
+
line = CanonicalLine(
|
|
201
|
+
canonical_id=f"line:{dn_pattern}:{partition}",
|
|
202
|
+
provenance=Provenance(
|
|
203
|
+
source_system="cucm",
|
|
204
|
+
source_id=dn_id,
|
|
205
|
+
source_name=dn_pattern,
|
|
206
|
+
extracted_at=datetime.now(timezone.utc),
|
|
207
|
+
),
|
|
208
|
+
status=MigrationStatus.ANALYZED,
|
|
209
|
+
extension=extension,
|
|
210
|
+
e164=e164,
|
|
211
|
+
classification=classification,
|
|
212
|
+
shared=shared,
|
|
213
|
+
cucm_pattern=dn_pattern,
|
|
214
|
+
route_partition_name=partition if partition != "<None>" else None,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
store.upsert_object(line)
|
|
218
|
+
result.objects_created += 1
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def _build_precomputed_e164_index(
|
|
223
|
+
self,
|
|
224
|
+
store: MigrationStore,
|
|
225
|
+
dn_to_devices: dict[str, list[str]],
|
|
226
|
+
precomputed: dict[str, dict[str, Any]],
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Build index of pre-computed e164_results from device line appearances.
|
|
229
|
+
|
|
230
|
+
After CrossReferenceBuilder runs, each line appearance entry in a device's
|
|
231
|
+
pre_migration_state has e164_result: {e164, extension, classification}.
|
|
232
|
+
We index these by DN ID so the mapper can use them directly.
|
|
233
|
+
|
|
234
|
+
(from Phase 04 corrections: line appearances have e164_result already computed)
|
|
235
|
+
"""
|
|
236
|
+
seen_devices: set[str] = set()
|
|
237
|
+
for dn_id, device_ids in dn_to_devices.items():
|
|
238
|
+
if dn_id in precomputed:
|
|
239
|
+
continue
|
|
240
|
+
for dev_id in device_ids:
|
|
241
|
+
if dev_id in seen_devices:
|
|
242
|
+
continue
|
|
243
|
+
seen_devices.add(dev_id)
|
|
244
|
+
# Handle virtual cross-ref IDs gracefully
|
|
245
|
+
# (from Phase 04 corrections: get_object may return None)
|
|
246
|
+
dev_data = store.get_object(dev_id)
|
|
247
|
+
if not dev_data:
|
|
248
|
+
continue
|
|
249
|
+
state = dev_data.get("pre_migration_state") or {}
|
|
250
|
+
for la in state.get("line_appearances", []):
|
|
251
|
+
la_dn_id = la.get("dn_canonical_id")
|
|
252
|
+
e164_res = la.get("e164_result")
|
|
253
|
+
if la_dn_id and e164_res and la_dn_id not in precomputed:
|
|
254
|
+
precomputed[la_dn_id] = e164_res
|
|
255
|
+
|
|
256
|
+
def _resolve_country_code(
|
|
257
|
+
self,
|
|
258
|
+
store: MigrationStore,
|
|
259
|
+
dn_id: str,
|
|
260
|
+
dn_to_devices: dict[str, list[str]],
|
|
261
|
+
) -> str:
|
|
262
|
+
"""Resolve country code for a DN by following the location chain.
|
|
263
|
+
|
|
264
|
+
Chain: DN -> (reverse device_has_dn) -> device -> device_in_pool
|
|
265
|
+
-> device_pool_to_location -> location -> address.country
|
|
266
|
+
|
|
267
|
+
(from 03b-transform-mappers.md §3: "country_code comes from the DN's resolved location")
|
|
268
|
+
|
|
269
|
+
Handles virtual cross-ref IDs gracefully — if get_object returns None
|
|
270
|
+
for any step, continues to the next device.
|
|
271
|
+
(from Phase 04 corrections: handle get_object returning None)
|
|
272
|
+
"""
|
|
273
|
+
device_ids = dn_to_devices.get(dn_id, [])
|
|
274
|
+
for dev_id in device_ids:
|
|
275
|
+
# Follow: device -> device_in_pool -> device_pool_to_location
|
|
276
|
+
location_id = store.resolve_chain(
|
|
277
|
+
dev_id, "device_in_pool", "device_pool_to_location"
|
|
278
|
+
)
|
|
279
|
+
if location_id:
|
|
280
|
+
loc_data = store.get_object(location_id)
|
|
281
|
+
if loc_data:
|
|
282
|
+
address = loc_data.get("address", {})
|
|
283
|
+
country = address.get("country")
|
|
284
|
+
if country:
|
|
285
|
+
return country
|
|
286
|
+
return self.default_country_code
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class _E164ResultProxy:
|
|
290
|
+
"""Lightweight proxy matching E164Result interface for pre-computed values.
|
|
291
|
+
|
|
292
|
+
Used when e164_result is already available from CrossReferenceBuilder
|
|
293
|
+
rather than calling normalize_dn().
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
__slots__ = ("e164", "extension", "raw", "classification")
|
|
297
|
+
|
|
298
|
+
def __init__(self, e164: str | None, extension: str | None, raw: str, classification: str):
|
|
299
|
+
self.e164 = e164
|
|
300
|
+
self.extension = extension
|
|
301
|
+
self.raw = raw
|
|
302
|
+
self.classification = classification
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _classify(classification_str: str) -> LineClassification:
|
|
306
|
+
"""Convert E164Result classification string to LineClassification enum."""
|
|
307
|
+
try:
|
|
308
|
+
return LineClassification(classification_str)
|
|
309
|
+
except ValueError:
|
|
310
|
+
return LineClassification.AMBIGUOUS
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Location mapper: CUCM Device Pools -> Webex Calling Locations.
|
|
2
|
+
|
|
3
|
+
Maps one or more CUCM device pools to a single CanonicalLocation, consolidated
|
|
4
|
+
by the CUCM Location entity they reference. Multiple device pools sharing the
|
|
5
|
+
same CUCM Location produce exactly one Webex location.
|
|
6
|
+
|
|
7
|
+
(from 03b-transform-mappers.md §1, location_mapper)
|
|
8
|
+
|
|
9
|
+
Cross-ref reads:
|
|
10
|
+
device_pool_has_datetime_group (DevicePool -> DateTimeGroup) — timezone
|
|
11
|
+
device_pool_at_cucm_location (DevicePool -> CUCM Location) — address
|
|
12
|
+
|
|
13
|
+
Cross-ref writes:
|
|
14
|
+
device_pool_to_location (DevicePool canonical_id -> new location canonical_id)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import logging
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from wxcli.migration.models import (
|
|
25
|
+
CanonicalLocation,
|
|
26
|
+
DecisionType,
|
|
27
|
+
LocationAddress,
|
|
28
|
+
MapperResult,
|
|
29
|
+
MigrationStatus,
|
|
30
|
+
Provenance,
|
|
31
|
+
)
|
|
32
|
+
from wxcli.migration.store import MigrationStore
|
|
33
|
+
from wxcli.migration.transform.mappers.base import (
|
|
34
|
+
Mapper,
|
|
35
|
+
decision_to_store_dict,
|
|
36
|
+
manual_option,
|
|
37
|
+
skip_option,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# Max location name length in Webex
|
|
43
|
+
# (from 03b-transform-mappers.md §1: "truncate to 80 chars")
|
|
44
|
+
_MAX_LOCATION_NAME_LENGTH = 80
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LocationMapper(Mapper):
|
|
48
|
+
"""Maps CUCM Device Pools to Webex Calling Locations.
|
|
49
|
+
|
|
50
|
+
(from 03b-transform-mappers.md §1)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name = "location_mapper"
|
|
54
|
+
depends_on: list[str] = []
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
default_language: str = "en_us",
|
|
59
|
+
default_country: str = "US",
|
|
60
|
+
outside_dial_digit: str = "9",
|
|
61
|
+
site_prefix_rules: list[dict[str, str]] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.default_language = default_language
|
|
64
|
+
self.default_country = default_country
|
|
65
|
+
self.outside_dial_digit = outside_dial_digit
|
|
66
|
+
self.site_prefix_rules = site_prefix_rules or []
|
|
67
|
+
|
|
68
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
69
|
+
"""Read device pools and produce CanonicalLocation objects.
|
|
70
|
+
|
|
71
|
+
Consolidation: device pools that share a CUCM Location entity are merged
|
|
72
|
+
into a single CanonicalLocation. The first device pool name is used as the
|
|
73
|
+
location name; all pool names are stored in cucm_device_pool_names.
|
|
74
|
+
|
|
75
|
+
(from 03b-transform-mappers.md §1, consolidation edge case)
|
|
76
|
+
"""
|
|
77
|
+
result = MapperResult()
|
|
78
|
+
|
|
79
|
+
# Phase 1: Group device pools by their CUCM Location reference
|
|
80
|
+
# Key = cucm_location canonical_id, Value = list of device pool dicts
|
|
81
|
+
location_groups: dict[str, list[dict[str, Any]]] = {}
|
|
82
|
+
# Track device pools with no CUCM location reference
|
|
83
|
+
orphan_pools: list[dict[str, Any]] = []
|
|
84
|
+
|
|
85
|
+
for dp_data in store.get_objects("device_pool"):
|
|
86
|
+
dp_id = dp_data["canonical_id"]
|
|
87
|
+
|
|
88
|
+
# Resolve CUCM Location via cross-ref
|
|
89
|
+
loc_refs = store.find_cross_refs(dp_id, "device_pool_at_cucm_location")
|
|
90
|
+
if loc_refs:
|
|
91
|
+
cucm_loc_id = loc_refs[0]
|
|
92
|
+
location_groups.setdefault(cucm_loc_id, []).append(dp_data)
|
|
93
|
+
else:
|
|
94
|
+
orphan_pools.append(dp_data)
|
|
95
|
+
|
|
96
|
+
# Phase 2: Create one CanonicalLocation per CUCM Location group
|
|
97
|
+
for cucm_loc_id, dp_list in location_groups.items():
|
|
98
|
+
location, warnings = self._build_location(store, cucm_loc_id, dp_list)
|
|
99
|
+
for w in warnings:
|
|
100
|
+
location.warnings.append(w)
|
|
101
|
+
|
|
102
|
+
store.upsert_object(location)
|
|
103
|
+
result.objects_created += 1
|
|
104
|
+
|
|
105
|
+
# Write device_pool_to_location cross-refs for all pools in the group
|
|
106
|
+
for dp_data in dp_list:
|
|
107
|
+
store.add_cross_ref(
|
|
108
|
+
dp_data["canonical_id"],
|
|
109
|
+
location.canonical_id,
|
|
110
|
+
"device_pool_to_location",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Phase 3: Handle orphan device pools (no CUCM Location ref)
|
|
114
|
+
for dp_data in orphan_pools:
|
|
115
|
+
dp_id = dp_data["canonical_id"]
|
|
116
|
+
dp_name = dp_data.get("pre_migration_state", {}).get(
|
|
117
|
+
"device_pool_name", dp_id
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
decision = self._create_decision(
|
|
121
|
+
store=store,
|
|
122
|
+
decision_type=DecisionType.LOCATION_AMBIGUOUS,
|
|
123
|
+
severity="HIGH",
|
|
124
|
+
summary=f"Device pool '{dp_name}' has no CUCM Location reference — cannot determine Webex location",
|
|
125
|
+
context={
|
|
126
|
+
"device_pool_id": dp_id,
|
|
127
|
+
"device_pool_name": dp_name,
|
|
128
|
+
},
|
|
129
|
+
options=[
|
|
130
|
+
manual_option("Admin must provide address and create location manually"),
|
|
131
|
+
skip_option("Device pool not migrated; users/devices in this pool need reassignment"),
|
|
132
|
+
],
|
|
133
|
+
affected_objects=[dp_id],
|
|
134
|
+
)
|
|
135
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
136
|
+
result.decisions.append(decision)
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def _build_location(
|
|
141
|
+
self,
|
|
142
|
+
store: MigrationStore,
|
|
143
|
+
cucm_loc_id: str,
|
|
144
|
+
dp_list: list[dict[str, Any]],
|
|
145
|
+
) -> tuple[CanonicalLocation, list[str]]:
|
|
146
|
+
"""Build a CanonicalLocation from a group of device pools sharing a CUCM Location.
|
|
147
|
+
|
|
148
|
+
Returns the location and a list of warning strings.
|
|
149
|
+
"""
|
|
150
|
+
warnings: list[str] = []
|
|
151
|
+
|
|
152
|
+
# Use first device pool as primary (for naming)
|
|
153
|
+
primary_dp = dp_list[0]
|
|
154
|
+
primary_dp_state = primary_dp.get("pre_migration_state", {})
|
|
155
|
+
dp_names = [
|
|
156
|
+
dp.get("pre_migration_state", {}).get("device_pool_name", dp["canonical_id"])
|
|
157
|
+
for dp in dp_list
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Location name from primary device pool, truncated to 80 chars
|
|
161
|
+
# (from 03b-transform-mappers.md §1: "truncate to 80 chars")
|
|
162
|
+
name = dp_names[0]
|
|
163
|
+
if len(name) > _MAX_LOCATION_NAME_LENGTH:
|
|
164
|
+
warnings.append(
|
|
165
|
+
f"Location name truncated from {len(name)} to {_MAX_LOCATION_NAME_LENGTH} chars: '{name}'"
|
|
166
|
+
)
|
|
167
|
+
name = name[:_MAX_LOCATION_NAME_LENGTH]
|
|
168
|
+
|
|
169
|
+
# Resolve timezone via device_pool_has_datetime_group cross-ref
|
|
170
|
+
# (from 03b-transform-mappers.md §1: datetime group -> IANA timezone)
|
|
171
|
+
time_zone = None
|
|
172
|
+
dtg_name = None
|
|
173
|
+
dtg_refs = store.find_cross_refs(
|
|
174
|
+
primary_dp["canonical_id"], "device_pool_has_datetime_group"
|
|
175
|
+
)
|
|
176
|
+
if dtg_refs:
|
|
177
|
+
dtg_data = store.get_object(dtg_refs[0])
|
|
178
|
+
if dtg_data:
|
|
179
|
+
dtg_state = dtg_data.get("pre_migration_state", {})
|
|
180
|
+
time_zone = dtg_state.get("timezone")
|
|
181
|
+
dtg_name = dtg_state.get("datetime_group_name")
|
|
182
|
+
|
|
183
|
+
# Resolve address from CUCM Location entity
|
|
184
|
+
# The cucm_loc_id is like "cucm_location:SomeName" — may or may not be in the store
|
|
185
|
+
address = LocationAddress(country=self.default_country)
|
|
186
|
+
cucm_loc_data = store.get_object(cucm_loc_id)
|
|
187
|
+
if cucm_loc_data:
|
|
188
|
+
loc_state = cucm_loc_data.get("pre_migration_state", {})
|
|
189
|
+
address = LocationAddress(
|
|
190
|
+
address1=loc_state.get("address1"),
|
|
191
|
+
city=loc_state.get("city"),
|
|
192
|
+
state=loc_state.get("state"),
|
|
193
|
+
postal_code=loc_state.get("postal_code"),
|
|
194
|
+
country=loc_state.get("country") or self.default_country,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Canonical ID: single-source if one pool, consolidated hash if multiple
|
|
198
|
+
# (from 03b-transform-mappers.md §13, Canonical ID Convention)
|
|
199
|
+
if len(dp_list) == 1:
|
|
200
|
+
canonical_id = f"location:{primary_dp['canonical_id'].replace('device_pool:', '')}"
|
|
201
|
+
else:
|
|
202
|
+
sorted_ids = sorted(dp["canonical_id"] for dp in dp_list)
|
|
203
|
+
hash_input = "|".join(sorted_ids)
|
|
204
|
+
hash_val = hashlib.sha256(hash_input.encode()).hexdigest()[:12]
|
|
205
|
+
canonical_id = f"location:{hash_val}"
|
|
206
|
+
|
|
207
|
+
# announcementLanguage must be lowercase
|
|
208
|
+
# (from 03b-transform-mappers.md §1: "Must be lowercase (en_us not en_US)")
|
|
209
|
+
# (from docs/reference/location-call-settings-core.md)
|
|
210
|
+
announcement_language = self.default_language.lower()
|
|
211
|
+
|
|
212
|
+
location = CanonicalLocation(
|
|
213
|
+
canonical_id=canonical_id,
|
|
214
|
+
provenance=Provenance(
|
|
215
|
+
source_system="cucm",
|
|
216
|
+
source_id=primary_dp.get("provenance", {}).get("source_id", ""),
|
|
217
|
+
source_name=dp_names[0],
|
|
218
|
+
cluster=primary_dp.get("provenance", {}).get("cluster"),
|
|
219
|
+
extracted_at=datetime.now(timezone.utc),
|
|
220
|
+
),
|
|
221
|
+
status=MigrationStatus.ANALYZED,
|
|
222
|
+
name=name,
|
|
223
|
+
time_zone=time_zone,
|
|
224
|
+
preferred_language=self.default_language.lower(),
|
|
225
|
+
announcement_language=announcement_language,
|
|
226
|
+
address=address,
|
|
227
|
+
routing_prefix=None, # Set from config if available
|
|
228
|
+
outside_dial_digit=str(self.outside_dial_digit) if self.outside_dial_digit is not None else None,
|
|
229
|
+
calling_enabled=True,
|
|
230
|
+
cucm_device_pool_names=dp_names,
|
|
231
|
+
cucm_datetime_group_name=dtg_name,
|
|
232
|
+
cucm_location_name=cucm_loc_id.replace("cucm_location:", ""),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return location, warnings
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""MOH mapper: CUCM MOH Audio Sources -> Webex per-location Music On Hold.
|
|
2
|
+
|
|
3
|
+
Maps CUCM Music On Hold audio sources to Webex Calling per-location MOH
|
|
4
|
+
settings (CanonicalMusicOnHold).
|
|
5
|
+
|
|
6
|
+
Default MOH sources map automatically. Custom audio sources produce
|
|
7
|
+
AUDIO_ASSET_MANUAL decisions because the WAV file must be manually
|
|
8
|
+
downloaded from CUCM and uploaded to Webex.
|
|
9
|
+
|
|
10
|
+
(from tier2-enterprise-expansion.md §2.3)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from wxcli.migration.models import (
|
|
19
|
+
CanonicalMusicOnHold,
|
|
20
|
+
DecisionType,
|
|
21
|
+
MapperResult,
|
|
22
|
+
MigrationStatus,
|
|
23
|
+
)
|
|
24
|
+
from wxcli.migration.store import MigrationStore
|
|
25
|
+
from wxcli.migration.transform.mappers.base import (
|
|
26
|
+
Mapper,
|
|
27
|
+
accept_option,
|
|
28
|
+
extract_provenance,
|
|
29
|
+
skip_option,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MOHMapper(Mapper):
|
|
36
|
+
"""Map CUCM MOH audio sources to Webex per-location MOH settings.
|
|
37
|
+
|
|
38
|
+
For each moh_source in the store:
|
|
39
|
+
- Default sources: create CanonicalMusicOnHold, no decision needed
|
|
40
|
+
- Custom sources: create CanonicalMusicOnHold + AUDIO_ASSET_MANUAL decision
|
|
41
|
+
|
|
42
|
+
(from tier2-enterprise-expansion.md §2.3)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name = "moh_mapper"
|
|
46
|
+
depends_on = ["location_mapper"]
|
|
47
|
+
|
|
48
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
49
|
+
"""Read CUCM MOH sources and produce Webex MOH settings."""
|
|
50
|
+
result = MapperResult()
|
|
51
|
+
|
|
52
|
+
for moh_data in store.get_objects("moh_source"):
|
|
53
|
+
canonical_id = moh_data["canonical_id"]
|
|
54
|
+
state = moh_data.get("pre_migration_state") or {}
|
|
55
|
+
|
|
56
|
+
name = state.get("name") or ""
|
|
57
|
+
source_file_name = state.get("source_file_name") or ""
|
|
58
|
+
is_default = state.get("is_default", False)
|
|
59
|
+
source_id = state.get("source_id") or ""
|
|
60
|
+
|
|
61
|
+
prov = extract_provenance(moh_data)
|
|
62
|
+
|
|
63
|
+
moh_obj = CanonicalMusicOnHold(
|
|
64
|
+
canonical_id=f"music_on_hold:{name}",
|
|
65
|
+
provenance=prov,
|
|
66
|
+
status=MigrationStatus.ANALYZED,
|
|
67
|
+
source_name=name,
|
|
68
|
+
source_file_name=source_file_name,
|
|
69
|
+
is_default=is_default,
|
|
70
|
+
cucm_source_id=source_id,
|
|
71
|
+
)
|
|
72
|
+
store.upsert_object(moh_obj)
|
|
73
|
+
result.objects_created += 1
|
|
74
|
+
|
|
75
|
+
# Custom audio sources need manual intervention
|
|
76
|
+
if not is_default:
|
|
77
|
+
decision = self._create_decision(
|
|
78
|
+
store=store,
|
|
79
|
+
decision_type=DecisionType.AUDIO_ASSET_MANUAL,
|
|
80
|
+
severity="MEDIUM",
|
|
81
|
+
summary=(
|
|
82
|
+
f"Custom MOH audio source '{name}' "
|
|
83
|
+
f"(file: {source_file_name or 'unknown'}) "
|
|
84
|
+
f"requires manual download from CUCM and upload to Webex"
|
|
85
|
+
),
|
|
86
|
+
context={
|
|
87
|
+
"moh_source_name": name,
|
|
88
|
+
"source_file_name": source_file_name,
|
|
89
|
+
"source_id": source_id,
|
|
90
|
+
"canonical_id": f"music_on_hold:{name}",
|
|
91
|
+
},
|
|
92
|
+
options=[
|
|
93
|
+
accept_option(
|
|
94
|
+
"Admin downloads WAV from CUCM and uploads to Webex location MOH"
|
|
95
|
+
),
|
|
96
|
+
_use_default_option(),
|
|
97
|
+
skip_option("Skip MOH migration for this source"),
|
|
98
|
+
],
|
|
99
|
+
affected_objects=[f"music_on_hold:{name}"],
|
|
100
|
+
)
|
|
101
|
+
result.decisions.append(decision)
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _use_default_option():
|
|
107
|
+
"""Create a 'Use default MOH' decision option."""
|
|
108
|
+
from wxcli.migration.models import DecisionOption
|
|
109
|
+
|
|
110
|
+
return DecisionOption(
|
|
111
|
+
id="use_default",
|
|
112
|
+
label="Use Webex default",
|
|
113
|
+
impact="Accept Webex default MOH instead of custom audio",
|
|
114
|
+
)
|