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,190 @@
|
|
|
1
|
+
"""Base class for all transform mappers.
|
|
2
|
+
|
|
3
|
+
Provides the Mapper ABC with map() abstract method, decision creation helpers,
|
|
4
|
+
fingerprint generation, and standard decision option builders.
|
|
5
|
+
|
|
6
|
+
(from 03b-transform-mappers.md, shared patterns section §13)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from wxcli.migration.models import (
|
|
18
|
+
CanonicalUser,
|
|
19
|
+
Decision,
|
|
20
|
+
DecisionOption,
|
|
21
|
+
DecisionType,
|
|
22
|
+
MapperResult,
|
|
23
|
+
Provenance,
|
|
24
|
+
)
|
|
25
|
+
from wxcli.migration.store import MigrationStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Mapper(ABC):
|
|
29
|
+
"""Base class for all transform mappers.
|
|
30
|
+
|
|
31
|
+
Each mapper reads CUCM canonical objects from the store, produces
|
|
32
|
+
Webex canonical objects, and returns decisions for anything it
|
|
33
|
+
can't cleanly map.
|
|
34
|
+
|
|
35
|
+
(from 03b-transform-mappers.md, shared patterns — Mapper base class)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str = ""
|
|
39
|
+
depends_on: list[str] = []
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
43
|
+
"""Read CUCM canonical objects, produce Webex canonical objects + decisions."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def _create_decision(
|
|
47
|
+
self,
|
|
48
|
+
store: MigrationStore,
|
|
49
|
+
decision_type: DecisionType,
|
|
50
|
+
severity: str,
|
|
51
|
+
summary: str,
|
|
52
|
+
context: dict[str, Any],
|
|
53
|
+
options: list[DecisionOption],
|
|
54
|
+
affected_objects: list[str],
|
|
55
|
+
) -> Decision:
|
|
56
|
+
"""Helper to create a well-formed Decision with auto-generated ID and fingerprint.
|
|
57
|
+
|
|
58
|
+
(from 03b-transform-mappers.md, shared patterns — _create_decision helper)
|
|
59
|
+
"""
|
|
60
|
+
return Decision(
|
|
61
|
+
decision_id=store.next_decision_id(),
|
|
62
|
+
type=decision_type,
|
|
63
|
+
severity=severity,
|
|
64
|
+
summary=summary,
|
|
65
|
+
context=context,
|
|
66
|
+
options=options,
|
|
67
|
+
affected_objects=affected_objects,
|
|
68
|
+
fingerprint=self._fingerprint(decision_type, context),
|
|
69
|
+
run_id=store.current_run_id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
|
|
73
|
+
"""Deterministic fingerprint for idempotent decision identity.
|
|
74
|
+
|
|
75
|
+
(from 03b-transform-mappers.md, shared patterns — _fingerprint helper)
|
|
76
|
+
"""
|
|
77
|
+
key_data = json.dumps({"type": decision_type.value, **context}, sort_keys=True)
|
|
78
|
+
return hashlib.sha256(key_data.encode()).hexdigest()[:16]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Decision option builder helpers
|
|
83
|
+
# (from 03b-transform-mappers.md, shared patterns — Decision Option Builder)
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def skip_option(impact: str = "Object not migrated") -> DecisionOption:
|
|
88
|
+
"""Create a 'Skip' decision option."""
|
|
89
|
+
return DecisionOption(id="skip", label="Skip", impact=impact)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def manual_option(impact: str = "Requires manual configuration post-migration") -> DecisionOption:
|
|
93
|
+
"""Create a 'Manual' decision option."""
|
|
94
|
+
return DecisionOption(id="manual", label="Manual", impact=impact)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def accept_option(impact: str) -> DecisionOption:
|
|
98
|
+
"""Create an 'Accept fidelity loss' decision option."""
|
|
99
|
+
return DecisionOption(id="accept", label="Accept fidelity loss", impact=impact)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Store serialization helper
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def decision_to_store_dict(decision: Decision) -> dict[str, Any]:
|
|
108
|
+
"""Convert a Decision Pydantic model to the dict format store.save_decision() expects.
|
|
109
|
+
|
|
110
|
+
The store expects:
|
|
111
|
+
- type as string value (e.g., "LOCATION_AMBIGUOUS"), not the enum member
|
|
112
|
+
- options as list of dicts (not DecisionOption models)
|
|
113
|
+
- context as dict (not JSON string — the store handles serialization)
|
|
114
|
+
|
|
115
|
+
affected_objects is embedded in context (as _affected_objects) since the
|
|
116
|
+
decisions table has no dedicated column. merge_decisions() in Phase 10
|
|
117
|
+
can read it from context when needed.
|
|
118
|
+
"""
|
|
119
|
+
ctx = dict(decision.context)
|
|
120
|
+
if decision.affected_objects:
|
|
121
|
+
ctx["_affected_objects"] = decision.affected_objects
|
|
122
|
+
return {
|
|
123
|
+
"decision_id": decision.decision_id,
|
|
124
|
+
"type": decision.type.value,
|
|
125
|
+
"severity": decision.severity,
|
|
126
|
+
"summary": decision.summary,
|
|
127
|
+
"context": ctx,
|
|
128
|
+
"options": [opt.model_dump() for opt in decision.options],
|
|
129
|
+
"chosen_option": decision.chosen_option,
|
|
130
|
+
"resolved_at": decision.resolved_at,
|
|
131
|
+
"resolved_by": decision.resolved_by,
|
|
132
|
+
"fingerprint": decision.fingerprint,
|
|
133
|
+
"run_id": decision.run_id,
|
|
134
|
+
"recommendation": decision.recommendation,
|
|
135
|
+
"recommendation_reasoning": decision.recommendation_reasoning,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Shared mapper utilities (DRY — used by all 9 mappers)
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def extract_provenance(data: dict[str, Any]) -> Provenance:
|
|
145
|
+
"""Extract Provenance from a stored object dict.
|
|
146
|
+
|
|
147
|
+
Handles both Pydantic-serialized dicts (from store.get_objects) and
|
|
148
|
+
raw dicts. Gracefully handles missing or malformed extracted_at.
|
|
149
|
+
"""
|
|
150
|
+
prov = data.get("provenance", {})
|
|
151
|
+
extracted_at = prov.get("extracted_at")
|
|
152
|
+
if isinstance(extracted_at, str):
|
|
153
|
+
try:
|
|
154
|
+
extracted_at = datetime.fromisoformat(extracted_at)
|
|
155
|
+
except (ValueError, TypeError):
|
|
156
|
+
extracted_at = datetime.now(timezone.utc)
|
|
157
|
+
elif extracted_at is None:
|
|
158
|
+
extracted_at = datetime.now(timezone.utc)
|
|
159
|
+
|
|
160
|
+
return Provenance(
|
|
161
|
+
source_system=prov.get("source_system", "cucm"),
|
|
162
|
+
source_id=prov.get("source_id", ""),
|
|
163
|
+
source_name=prov.get("source_name", ""),
|
|
164
|
+
cluster=prov.get("cluster"),
|
|
165
|
+
extracted_at=extracted_at,
|
|
166
|
+
cucm_version=prov.get("cucm_version"),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def enrich_user(store: MigrationStore, user_id: str, **fields: Any) -> None:
|
|
171
|
+
"""Update fields on a CanonicalUser in the store.
|
|
172
|
+
|
|
173
|
+
Re-reads the user dict, merges the provided fields, reconstructs
|
|
174
|
+
a CanonicalUser (filtering to known model fields), and upserts.
|
|
175
|
+
Used by enrichment mappers (VoicemailMapper, CallSettingsMapper).
|
|
176
|
+
"""
|
|
177
|
+
user_data = store.get_object(user_id)
|
|
178
|
+
if not user_data:
|
|
179
|
+
return
|
|
180
|
+
user_data.update(fields)
|
|
181
|
+
enriched = CanonicalUser(**{
|
|
182
|
+
k: v for k, v in user_data.items()
|
|
183
|
+
if k in CanonicalUser.model_fields
|
|
184
|
+
})
|
|
185
|
+
store.upsert_object(enriched)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def hash_id(value: str) -> str:
|
|
189
|
+
"""Generate a deterministic short hash for canonical IDs."""
|
|
190
|
+
return hashlib.sha256(value.encode()).hexdigest()[:12]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""ButtonTemplateMapper: CUCM Phone Button Templates -> Webex Line Key Templates.
|
|
2
|
+
|
|
3
|
+
Maps CUCM button feature types to Webex LineKeyType values. Determines KEM
|
|
4
|
+
boundaries from model-specific line key counts. Produces CanonicalLineKeyTemplate
|
|
5
|
+
objects and BUTTON_UNMAPPABLE decisions for unmappable buttons.
|
|
6
|
+
|
|
7
|
+
(from tier2-phase2-phone-config-design.md S4.1)
|
|
8
|
+
|
|
9
|
+
Cross-ref reads:
|
|
10
|
+
phone_uses_button_template (Phone -> ButtonTemplate) -- usage count
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from wxcli.migration.models import (
|
|
19
|
+
CanonicalLineKeyTemplate,
|
|
20
|
+
DecisionOption,
|
|
21
|
+
DecisionType,
|
|
22
|
+
MapperResult,
|
|
23
|
+
MigrationStatus,
|
|
24
|
+
)
|
|
25
|
+
from wxcli.migration.store import MigrationStore
|
|
26
|
+
from wxcli.migration.transform.mappers.base import (
|
|
27
|
+
Mapper,
|
|
28
|
+
decision_to_store_dict,
|
|
29
|
+
extract_provenance,
|
|
30
|
+
skip_option,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# CUCM button feature -> Webex LineKeyType
|
|
36
|
+
CUCM_BUTTON_TO_WEBEX: dict[str, str | None] = {
|
|
37
|
+
"Line": "PRIMARY_LINE",
|
|
38
|
+
"Speed Dial": "SPEED_DIAL",
|
|
39
|
+
"Busy Lamp Field": "MONITOR",
|
|
40
|
+
"Speed Dial BLF": "MONITOR", # AXL getPhoneButtonTemplate returns this name
|
|
41
|
+
"Call Park": "CALL_PARK_EXTENSION",
|
|
42
|
+
"Call Park BLF": "CALL_PARK_EXTENSION",
|
|
43
|
+
"Abbreviated Dial": "SPEED_DIAL",
|
|
44
|
+
"None": "OPEN",
|
|
45
|
+
"Unassigned": "OPEN",
|
|
46
|
+
# Unmappable -- return None
|
|
47
|
+
"Intercom": None,
|
|
48
|
+
"Service URL": None,
|
|
49
|
+
"Privacy": None,
|
|
50
|
+
"Malicious Call ID": None,
|
|
51
|
+
"Quality Report Tool": None,
|
|
52
|
+
"Do Not Disturb": None,
|
|
53
|
+
"Feature": None,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Max line key count per phone model (buttons beyond this are KEM keys)
|
|
57
|
+
from wxcli.migration.phone_models import MODEL_LINE_KEY_COUNTS as _MODEL_LINE_KEY_COUNTS
|
|
58
|
+
|
|
59
|
+
# Regex to extract 4-digit model number from base template name
|
|
60
|
+
_MODEL_RE = re.compile(r"(\d{4})")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_model_from_base_template(base_template: str | None) -> str | None:
|
|
64
|
+
"""Extract the 4-digit phone model from a CUCM base template name.
|
|
65
|
+
|
|
66
|
+
E.g. "Standard 8845" -> "8845", "Universal Phone Template" -> None
|
|
67
|
+
"""
|
|
68
|
+
if not base_template:
|
|
69
|
+
return None
|
|
70
|
+
match = _MODEL_RE.search(base_template)
|
|
71
|
+
return match.group(1) if match else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ButtonTemplateMapper(Mapper):
|
|
75
|
+
"""Maps CUCM Phone Button Templates to Webex Line Key Templates."""
|
|
76
|
+
|
|
77
|
+
name = "button_template_mapper"
|
|
78
|
+
depends_on = ["device_mapper"]
|
|
79
|
+
|
|
80
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
81
|
+
result = MapperResult()
|
|
82
|
+
|
|
83
|
+
for tmpl_data in store.get_objects("button_template"):
|
|
84
|
+
tmpl_id = tmpl_data["canonical_id"]
|
|
85
|
+
state = tmpl_data.get("pre_migration_state") or {}
|
|
86
|
+
name = state.get("name", tmpl_id.split(":", 1)[-1])
|
|
87
|
+
base_template = state.get("base_template")
|
|
88
|
+
buttons = state.get("buttons", [])
|
|
89
|
+
|
|
90
|
+
# Count phones using this template
|
|
91
|
+
phone_refs = [
|
|
92
|
+
r["from_id"]
|
|
93
|
+
for r in store.get_cross_refs(
|
|
94
|
+
to_id=tmpl_id, relationship="phone_uses_button_template"
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
phones_using = len(phone_refs)
|
|
98
|
+
|
|
99
|
+
# Determine model and max line keys
|
|
100
|
+
model_num = (
|
|
101
|
+
_parse_model_from_base_template(base_template)
|
|
102
|
+
or _parse_model_from_base_template(name)
|
|
103
|
+
)
|
|
104
|
+
max_keys = _MODEL_LINE_KEY_COUNTS.get(model_num, 999)
|
|
105
|
+
|
|
106
|
+
line_keys: list[dict[str, Any]] = []
|
|
107
|
+
kem_keys: list[dict[str, Any]] = []
|
|
108
|
+
unmapped: list[dict[str, Any]] = []
|
|
109
|
+
|
|
110
|
+
for btn in buttons:
|
|
111
|
+
idx = btn.get("index", 0)
|
|
112
|
+
feature = btn.get("feature", "")
|
|
113
|
+
webex_type = CUCM_BUTTON_TO_WEBEX.get(feature)
|
|
114
|
+
|
|
115
|
+
if webex_type is None and feature:
|
|
116
|
+
# Unknown or unmappable feature
|
|
117
|
+
unmapped.append({"index": idx, "feature": feature})
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if not webex_type:
|
|
121
|
+
webex_type = "OPEN"
|
|
122
|
+
|
|
123
|
+
entry = {"index": idx, "key_type": webex_type}
|
|
124
|
+
|
|
125
|
+
if idx <= max_keys:
|
|
126
|
+
line_keys.append(entry)
|
|
127
|
+
else:
|
|
128
|
+
kem_keys.append(entry)
|
|
129
|
+
|
|
130
|
+
# Determine KEM module type from key count
|
|
131
|
+
kem_module_type = None
|
|
132
|
+
if kem_keys:
|
|
133
|
+
kem_count = len(kem_keys)
|
|
134
|
+
if kem_count <= 14:
|
|
135
|
+
kem_module_type = "KEM_14_KEYS"
|
|
136
|
+
elif kem_count <= 18:
|
|
137
|
+
kem_module_type = "KEM_18_KEYS"
|
|
138
|
+
else:
|
|
139
|
+
kem_module_type = "KEM_20_KEYS"
|
|
140
|
+
|
|
141
|
+
lkt = CanonicalLineKeyTemplate(
|
|
142
|
+
canonical_id=f"line_key_template:{name}",
|
|
143
|
+
provenance=extract_provenance(tmpl_data),
|
|
144
|
+
status=MigrationStatus.ANALYZED,
|
|
145
|
+
name=name,
|
|
146
|
+
cucm_template_name=name,
|
|
147
|
+
device_model=f"DMS Cisco {model_num}" if model_num else None,
|
|
148
|
+
line_keys=line_keys,
|
|
149
|
+
kem_module_type=kem_module_type,
|
|
150
|
+
kem_keys=kem_keys,
|
|
151
|
+
unmapped_buttons=unmapped,
|
|
152
|
+
phones_using=phones_using,
|
|
153
|
+
)
|
|
154
|
+
store.upsert_object(lkt)
|
|
155
|
+
result.objects_created += 1
|
|
156
|
+
|
|
157
|
+
# Decision for unmappable buttons (only if phones use this template)
|
|
158
|
+
if unmapped and phones_using > 0:
|
|
159
|
+
features = list({u["feature"] for u in unmapped})
|
|
160
|
+
decision = self._create_decision(
|
|
161
|
+
store=store,
|
|
162
|
+
decision_type=DecisionType.BUTTON_UNMAPPABLE,
|
|
163
|
+
severity="LOW",
|
|
164
|
+
summary=(
|
|
165
|
+
f"Template '{name}' has {len(unmapped)} button(s) with no Webex "
|
|
166
|
+
f"equivalent: {', '.join(features)} ({phones_using} phones affected)"
|
|
167
|
+
),
|
|
168
|
+
context={
|
|
169
|
+
"template_name": name,
|
|
170
|
+
"unmapped_features": features,
|
|
171
|
+
"phones_using": phones_using,
|
|
172
|
+
},
|
|
173
|
+
options=[
|
|
174
|
+
DecisionOption(
|
|
175
|
+
id="accept_loss",
|
|
176
|
+
label="Accept loss",
|
|
177
|
+
impact="Accept -- these button types don't exist in Webex",
|
|
178
|
+
),
|
|
179
|
+
skip_option("Skip template migration for these phones"),
|
|
180
|
+
],
|
|
181
|
+
affected_objects=[lkt.canonical_id],
|
|
182
|
+
)
|
|
183
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
184
|
+
result.decisions.append(decision)
|
|
185
|
+
|
|
186
|
+
return result
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""CallForwardingMapper — CUCM per-line forwarding → Webex per-person call forwarding.
|
|
2
|
+
|
|
3
|
+
Reads phone objects from the store (which contain enriched line entries with
|
|
4
|
+
call forwarding fields from getLine), resolves user ownership via
|
|
5
|
+
device_owned_by_user cross-ref, and produces CanonicalCallForwarding objects.
|
|
6
|
+
|
|
7
|
+
CUCM has 10 forwarding variants. Webex supports 3 (CFA, CFB, CFNA).
|
|
8
|
+
The remaining 5 CUCM-only variants (BusyInt, NoAnswerInt, NoCoverage,
|
|
9
|
+
OnFailure, NotRegistered) produce FORWARDING_LOSSY decisions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import math
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from wxcli.migration.models import (
|
|
19
|
+
CanonicalCallForwarding,
|
|
20
|
+
DecisionType,
|
|
21
|
+
MigrationStatus,
|
|
22
|
+
)
|
|
23
|
+
from wxcli.migration.store import MigrationStore
|
|
24
|
+
from wxcli.migration.models import MapperResult
|
|
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
|
+
# CUCM-only forwarding variants that have no Webex equivalent
|
|
35
|
+
_LOSSY_VARIANTS = (
|
|
36
|
+
"callForwardBusyInt",
|
|
37
|
+
"callForwardNoAnswerInt",
|
|
38
|
+
"callForwardNoCoverage",
|
|
39
|
+
"callForwardNoCoverageInt",
|
|
40
|
+
"callForwardOnFailure",
|
|
41
|
+
"callForwardNotRegistered",
|
|
42
|
+
"callForwardNotRegisteredInt",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_forwarding_active(fwd: dict | None) -> bool:
|
|
47
|
+
"""Check if a CUCM forwarding setting is actively configured."""
|
|
48
|
+
if not fwd or not isinstance(fwd, dict):
|
|
49
|
+
return False
|
|
50
|
+
dest = fwd.get("destination")
|
|
51
|
+
fwd_to_vm = fwd.get("forwardToVoiceMail")
|
|
52
|
+
# Active if there's a non-empty destination or it forwards to voicemail
|
|
53
|
+
return bool(dest) or (fwd_to_vm in ("true", True))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _duration_to_rings(duration_ms: int | str | None) -> int | None:
|
|
57
|
+
"""Convert CUCM duration (seconds) to Webex ring count.
|
|
58
|
+
|
|
59
|
+
CUCM stores call forward no answer timer in seconds.
|
|
60
|
+
Webex uses ring count: divide by 6, round up, clamp to 2-20.
|
|
61
|
+
"""
|
|
62
|
+
if duration_ms is None:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
val = int(duration_ms)
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
return None
|
|
68
|
+
if val <= 0:
|
|
69
|
+
return None
|
|
70
|
+
rings = math.ceil(val / 6)
|
|
71
|
+
return max(2, min(20, rings))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CallForwardingMapper(Mapper):
|
|
75
|
+
"""Map CUCM per-line call forwarding to Webex per-person call forwarding.
|
|
76
|
+
|
|
77
|
+
Tier 2 expansion: adds CanonicalCallForwarding objects and
|
|
78
|
+
FORWARDING_LOSSY decisions for CUCM-only variants.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name = "call_forwarding_mapper"
|
|
82
|
+
depends_on = ["user_mapper", "line_mapper"]
|
|
83
|
+
|
|
84
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
85
|
+
result = MapperResult()
|
|
86
|
+
|
|
87
|
+
# Iterate phones (which contain enriched line entries)
|
|
88
|
+
for phone_data in store.get_objects("phone"):
|
|
89
|
+
phone_id = phone_data["canonical_id"]
|
|
90
|
+
state = phone_data.get("pre_migration_state") or {}
|
|
91
|
+
lines = state.get("lines", [])
|
|
92
|
+
if not lines:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Resolve owner user via cross-ref
|
|
96
|
+
user_refs = store.find_cross_refs(phone_id, "device_owned_by_user")
|
|
97
|
+
if not user_refs:
|
|
98
|
+
continue
|
|
99
|
+
user_id = user_refs[0]
|
|
100
|
+
|
|
101
|
+
# Check if we already created forwarding for this user (multi-device)
|
|
102
|
+
user_name = user_id.split(":", 1)[-1]
|
|
103
|
+
existing = store.get_object(f"call_forwarding:{user_name}")
|
|
104
|
+
if existing:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Use primary line (index 1 or first line)
|
|
108
|
+
primary_line = self._get_primary_line(lines)
|
|
109
|
+
if not primary_line:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Build canonical call forwarding
|
|
113
|
+
cf = self._build_call_forwarding(store, user_id, phone_data, primary_line)
|
|
114
|
+
store.upsert_object(cf)
|
|
115
|
+
result.objects_created += 1
|
|
116
|
+
|
|
117
|
+
# Write cross-ref
|
|
118
|
+
store.add_cross_ref(user_id, cf.canonical_id, "user_has_call_forwarding")
|
|
119
|
+
|
|
120
|
+
# Check for lossy CUCM-only variants
|
|
121
|
+
lossy_variants = self._detect_lossy_variants(primary_line)
|
|
122
|
+
if lossy_variants:
|
|
123
|
+
decision = self._create_decision(
|
|
124
|
+
store=store,
|
|
125
|
+
decision_type=DecisionType.FORWARDING_LOSSY,
|
|
126
|
+
severity="LOW",
|
|
127
|
+
summary=(
|
|
128
|
+
f"User '{user_name}' has {len(lossy_variants)} "
|
|
129
|
+
f"CUCM-only forwarding variant(s): {', '.join(lossy_variants)}"
|
|
130
|
+
),
|
|
131
|
+
context={
|
|
132
|
+
"user_id": user_id,
|
|
133
|
+
"lossy_variants": lossy_variants,
|
|
134
|
+
},
|
|
135
|
+
options=[
|
|
136
|
+
accept_option(f"Accept loss of {len(lossy_variants)} CUCM-only forwarding variant(s)"),
|
|
137
|
+
skip_option("Skip call forwarding migration for this user"),
|
|
138
|
+
],
|
|
139
|
+
affected_objects=[user_id, cf.canonical_id],
|
|
140
|
+
)
|
|
141
|
+
result.decisions.append(decision)
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
def _get_primary_line(self, lines: list | dict) -> dict | None:
|
|
146
|
+
"""Get the primary line (index 1) from a phone's line list or dict."""
|
|
147
|
+
if not lines:
|
|
148
|
+
return None
|
|
149
|
+
# lines may be a list or a dict (integer-keyed) depending on AXL normalization
|
|
150
|
+
if isinstance(lines, dict):
|
|
151
|
+
# Integer-keyed dict: try key 1 or "1"
|
|
152
|
+
first = lines.get(1) or lines.get("1")
|
|
153
|
+
return first if isinstance(first, dict) else None
|
|
154
|
+
# List form
|
|
155
|
+
for line in lines:
|
|
156
|
+
if isinstance(line, dict):
|
|
157
|
+
idx = line.get("index")
|
|
158
|
+
if str(idx) == "1":
|
|
159
|
+
return line
|
|
160
|
+
# Fallback: first line
|
|
161
|
+
first = lines[0]
|
|
162
|
+
return first if isinstance(first, dict) else None
|
|
163
|
+
|
|
164
|
+
def _build_call_forwarding(
|
|
165
|
+
self,
|
|
166
|
+
store: MigrationStore,
|
|
167
|
+
user_id: str,
|
|
168
|
+
phone_data: dict,
|
|
169
|
+
line: dict,
|
|
170
|
+
) -> CanonicalCallForwarding:
|
|
171
|
+
"""Build a CanonicalCallForwarding from a phone's primary line."""
|
|
172
|
+
user_name = user_id.split(":", 1)[-1]
|
|
173
|
+
prov = extract_provenance(phone_data)
|
|
174
|
+
|
|
175
|
+
# CFA (Call Forward All)
|
|
176
|
+
cfa = line.get("callForwardAll") or {}
|
|
177
|
+
# CFB (Call Forward Busy)
|
|
178
|
+
cfb = line.get("callForwardBusy") or {}
|
|
179
|
+
# CFNA (Call Forward No Answer)
|
|
180
|
+
cfna = line.get("callForwardNoAnswer") or {}
|
|
181
|
+
# CUCM-only variants (preserved for decision context)
|
|
182
|
+
cfb_int = line.get("callForwardBusyInt") or {}
|
|
183
|
+
cfna_int = line.get("callForwardNoAnswerInt") or {}
|
|
184
|
+
cfnc = line.get("callForwardNoCoverage") or {}
|
|
185
|
+
cfnc_int = line.get("callForwardNoCoverageInt") or {}
|
|
186
|
+
cff = line.get("callForwardOnFailure") or {}
|
|
187
|
+
cfnr = line.get("callForwardNotRegistered") or {}
|
|
188
|
+
cfnr_int = line.get("callForwardNotRegisteredInt") or {}
|
|
189
|
+
|
|
190
|
+
return CanonicalCallForwarding(
|
|
191
|
+
canonical_id=f"call_forwarding:{user_name}",
|
|
192
|
+
provenance=prov,
|
|
193
|
+
status=MigrationStatus.ANALYZED,
|
|
194
|
+
user_canonical_id=user_id,
|
|
195
|
+
# CFA
|
|
196
|
+
always_enabled=bool(cfa.get("destination") or cfa.get("forwardToVoiceMail") in ("true", True)),
|
|
197
|
+
always_destination=cfa.get("destination") or None,
|
|
198
|
+
always_to_voicemail=cfa.get("forwardToVoiceMail") in ("true", True),
|
|
199
|
+
# CFB
|
|
200
|
+
busy_enabled=bool(cfb.get("destination") or cfb.get("forwardToVoiceMail") in ("true", True)),
|
|
201
|
+
busy_destination=cfb.get("destination") or None,
|
|
202
|
+
busy_to_voicemail=cfb.get("forwardToVoiceMail") in ("true", True),
|
|
203
|
+
# CFNA
|
|
204
|
+
no_answer_enabled=bool(cfna.get("destination") or cfna.get("forwardToVoiceMail") in ("true", True)),
|
|
205
|
+
no_answer_destination=cfna.get("destination") or None,
|
|
206
|
+
no_answer_to_voicemail=cfna.get("forwardToVoiceMail") in ("true", True),
|
|
207
|
+
no_answer_ring_count=_duration_to_rings(cfna.get("duration")),
|
|
208
|
+
# CUCM-only (preserved in canonical for reporting)
|
|
209
|
+
busy_internal_enabled=_is_forwarding_active(cfb_int),
|
|
210
|
+
busy_internal_destination=cfb_int.get("destination") or None,
|
|
211
|
+
no_answer_internal_enabled=_is_forwarding_active(cfna_int),
|
|
212
|
+
no_answer_internal_destination=cfna_int.get("destination") or None,
|
|
213
|
+
no_coverage_enabled=_is_forwarding_active(cfnc) or _is_forwarding_active(cfnc_int),
|
|
214
|
+
no_coverage_destination=cfnc.get("destination") or cfnc_int.get("destination") or None,
|
|
215
|
+
on_failure_enabled=_is_forwarding_active(cff),
|
|
216
|
+
on_failure_destination=cff.get("destination") or None,
|
|
217
|
+
not_registered_enabled=_is_forwarding_active(cfnr) or _is_forwarding_active(cfnr_int),
|
|
218
|
+
not_registered_destination=cfnr.get("destination") or cfnr_int.get("destination") or None,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _detect_lossy_variants(self, line: dict) -> list[str]:
|
|
222
|
+
"""Detect CUCM-only forwarding variants that are actively configured."""
|
|
223
|
+
lossy = []
|
|
224
|
+
for variant in _LOSSY_VARIANTS:
|
|
225
|
+
fwd = line.get(variant)
|
|
226
|
+
if _is_forwarding_active(fwd):
|
|
227
|
+
lossy.append(variant)
|
|
228
|
+
return lossy
|