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,227 @@
|
|
|
1
|
+
"""Feature approximation analyzer for CUCM-to-Webex migration.
|
|
2
|
+
|
|
3
|
+
Finds CTI route points mapped to auto attendants with complex/unmappable
|
|
4
|
+
scripts, hunt pilots with non-standard algorithms, and call queues with
|
|
5
|
+
agent limit warnings that require approximation in Webex.
|
|
6
|
+
|
|
7
|
+
FEATURE_APPROXIMATION is a mapper-owned decision type — the feature_mapper
|
|
8
|
+
and routing_mapper already produce these during transform. This analyzer
|
|
9
|
+
backfills any objects the mappers missed or that acquired warnings after
|
|
10
|
+
initial mapping.
|
|
11
|
+
|
|
12
|
+
(from 03-conflict-detection-engine.md, linter pattern)
|
|
13
|
+
(from phase-06-analyzers.md, Decision Ownership Table — FEATURE_APPROXIMATION is mapper-owned)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from wxcli.migration.models import DecisionOption, DecisionType
|
|
21
|
+
from wxcli.migration.store import MigrationStore
|
|
22
|
+
from wxcli.migration.transform.analyzers import Analyzer, Decision
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Approximation detection keywords
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# These appear in warnings/errors emitted by feature_mapper and routing_mapper
|
|
29
|
+
# when a CUCM construct cannot be directly mapped to Webex.
|
|
30
|
+
|
|
31
|
+
_APPROXIMATION_KEYWORDS = (
|
|
32
|
+
"approximat",
|
|
33
|
+
"complex script",
|
|
34
|
+
"fidelity",
|
|
35
|
+
"not supported",
|
|
36
|
+
"agent limit",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extract_approximation_issues(
|
|
41
|
+
warnings: list[Any], errors: list[Any]
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
"""Extract warning/error strings that indicate approximation was needed."""
|
|
44
|
+
issues: list[str] = []
|
|
45
|
+
for source in (warnings, errors):
|
|
46
|
+
for item in source:
|
|
47
|
+
if not isinstance(item, str):
|
|
48
|
+
continue
|
|
49
|
+
item_lower = item.lower()
|
|
50
|
+
if any(kw in item_lower for kw in _APPROXIMATION_KEYWORDS):
|
|
51
|
+
issues.append(item)
|
|
52
|
+
return issues
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FeatureApproximationAnalyzer(Analyzer):
|
|
56
|
+
"""Detects mapped features that required approximation from CUCM to Webex.
|
|
57
|
+
|
|
58
|
+
Sweeps three object types for approximation indicators:
|
|
59
|
+
|
|
60
|
+
- **auto_attendant** — CTI route points mapped to AA; checks for
|
|
61
|
+
unmappable scripts (business_hours_menu or after_hours_menu is None)
|
|
62
|
+
and warning strings about script approximation.
|
|
63
|
+
|
|
64
|
+
- **hunt_group** — Hunt pilots with non-standard algorithms; checks
|
|
65
|
+
for warning strings about algorithm approximation.
|
|
66
|
+
|
|
67
|
+
- **call_queue** — Queues that exceeded Webex agent limits or had
|
|
68
|
+
unsupported features; checks for agent limit warnings.
|
|
69
|
+
|
|
70
|
+
Skips objects that already have a mapper-produced FEATURE_APPROXIMATION
|
|
71
|
+
decision.
|
|
72
|
+
|
|
73
|
+
(from 03-conflict-detection-engine.md, feature approximation linter)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name = "feature_approximation"
|
|
77
|
+
decision_types = [DecisionType.FEATURE_APPROXIMATION]
|
|
78
|
+
depends_on: list[str] = []
|
|
79
|
+
|
|
80
|
+
# Object types to sweep, with type-specific checks
|
|
81
|
+
_FEATURE_TYPES = ("auto_attendant", "hunt_group", "call_queue")
|
|
82
|
+
|
|
83
|
+
def analyze(self, store: MigrationStore) -> list[Decision]:
|
|
84
|
+
"""Sweep AA, HG, and CQ objects for approximation indicators."""
|
|
85
|
+
# Existing mapper-produced decisions indexed by canonical_id
|
|
86
|
+
existing = self._get_existing_decisions_for_type(
|
|
87
|
+
store, DecisionType.FEATURE_APPROXIMATION
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
decisions: list[Decision] = []
|
|
91
|
+
|
|
92
|
+
for feature_type in self._FEATURE_TYPES:
|
|
93
|
+
objects = store.get_objects(feature_type)
|
|
94
|
+
for obj in objects:
|
|
95
|
+
canonical_id = obj.get("canonical_id", "")
|
|
96
|
+
|
|
97
|
+
# Skip if mapper already produced a decision for this object
|
|
98
|
+
if canonical_id in existing:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
issues = self._detect_approximation_issues(feature_type, obj)
|
|
102
|
+
if not issues:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
severity = self._determine_severity(feature_type, obj, issues)
|
|
106
|
+
name = obj.get("name") or obj.get("display_name") or canonical_id
|
|
107
|
+
|
|
108
|
+
summary = (
|
|
109
|
+
f"{feature_type} '{name}' requires approximation: "
|
|
110
|
+
f"{'; '.join(issues[:3])}"
|
|
111
|
+
)
|
|
112
|
+
if len(issues) > 3:
|
|
113
|
+
summary += f" (+{len(issues) - 3} more)"
|
|
114
|
+
|
|
115
|
+
context = {
|
|
116
|
+
"feature_type": feature_type,
|
|
117
|
+
"canonical_id": canonical_id,
|
|
118
|
+
"name": name,
|
|
119
|
+
"approximation_issues": issues,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
options = [
|
|
123
|
+
DecisionOption(
|
|
124
|
+
id="accept",
|
|
125
|
+
label="Accept approximation",
|
|
126
|
+
impact=(
|
|
127
|
+
f"{feature_type} '{name}' migrated with best-effort "
|
|
128
|
+
f"approximation ({len(issues)} issue(s))"
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
DecisionOption(
|
|
132
|
+
id="manual",
|
|
133
|
+
label="Configure manually post-migration",
|
|
134
|
+
impact=(
|
|
135
|
+
f"{feature_type} '{name}' flagged for manual "
|
|
136
|
+
f"post-migration configuration"
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
DecisionOption(
|
|
140
|
+
id="skip",
|
|
141
|
+
label="Skip this feature",
|
|
142
|
+
impact=f"{feature_type} '{name}' excluded from migration",
|
|
143
|
+
),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
decision = self._create_decision(
|
|
147
|
+
store=store,
|
|
148
|
+
decision_type=DecisionType.FEATURE_APPROXIMATION,
|
|
149
|
+
severity=severity,
|
|
150
|
+
summary=summary,
|
|
151
|
+
context=context,
|
|
152
|
+
options=options,
|
|
153
|
+
affected_objects=[canonical_id],
|
|
154
|
+
)
|
|
155
|
+
decisions.append(decision)
|
|
156
|
+
|
|
157
|
+
return decisions
|
|
158
|
+
|
|
159
|
+
def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
|
|
160
|
+
"""Deterministic fingerprint from canonical_id + sorted approximation issues."""
|
|
161
|
+
return self._hash_fingerprint({
|
|
162
|
+
"type": decision_type.value,
|
|
163
|
+
"canonical_id": context.get("canonical_id", ""),
|
|
164
|
+
"approximation_issues": sorted(context.get("approximation_issues", [])),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Internal helpers
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _detect_approximation_issues(
|
|
173
|
+
feature_type: str, obj: dict[str, Any]
|
|
174
|
+
) -> list[str]:
|
|
175
|
+
"""Detect all approximation issues for a given feature object.
|
|
176
|
+
|
|
177
|
+
Checks warnings/errors lists for approximation keywords, plus
|
|
178
|
+
type-specific structural checks (e.g., None menus on auto_attendants).
|
|
179
|
+
"""
|
|
180
|
+
warnings = obj.get("warnings") or []
|
|
181
|
+
errors = obj.get("errors") or []
|
|
182
|
+
issues = _extract_approximation_issues(warnings, errors)
|
|
183
|
+
|
|
184
|
+
# Type-specific structural checks
|
|
185
|
+
if feature_type == "auto_attendant":
|
|
186
|
+
# None menus indicate an unmappable CTI route point script
|
|
187
|
+
if obj.get("business_hours_menu") is None:
|
|
188
|
+
issue = "business_hours_menu is None (unmappable CTI route point script)"
|
|
189
|
+
if issue not in issues:
|
|
190
|
+
issues.append(issue)
|
|
191
|
+
if obj.get("after_hours_menu") is None:
|
|
192
|
+
issue = "after_hours_menu is None (unmappable CTI route point script)"
|
|
193
|
+
if issue not in issues:
|
|
194
|
+
issues.append(issue)
|
|
195
|
+
|
|
196
|
+
return issues
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _determine_severity(
|
|
200
|
+
feature_type: str,
|
|
201
|
+
obj: dict[str, Any],
|
|
202
|
+
issues: list[str],
|
|
203
|
+
) -> str:
|
|
204
|
+
"""Determine decision severity based on feature type and issues.
|
|
205
|
+
|
|
206
|
+
HIGH: auto_attendant with None menus (completely unmappable)
|
|
207
|
+
MEDIUM: call_queue agent limit issues, hunt_group algorithm issues
|
|
208
|
+
LOW: minor approximations (warning-only, no structural problems)
|
|
209
|
+
"""
|
|
210
|
+
if feature_type == "auto_attendant":
|
|
211
|
+
# Unmappable menus are HIGH severity
|
|
212
|
+
if obj.get("business_hours_menu") is None or obj.get("after_hours_menu") is None:
|
|
213
|
+
return "HIGH"
|
|
214
|
+
|
|
215
|
+
# Agent limit issues are MEDIUM
|
|
216
|
+
for issue in issues:
|
|
217
|
+
if "agent limit" in issue.lower():
|
|
218
|
+
return "MEDIUM"
|
|
219
|
+
|
|
220
|
+
# Check for strong approximation keywords -> MEDIUM
|
|
221
|
+
for issue in issues:
|
|
222
|
+
issue_lower = issue.lower()
|
|
223
|
+
if any(kw in issue_lower for kw in ("complex script", "not supported")):
|
|
224
|
+
return "MEDIUM"
|
|
225
|
+
|
|
226
|
+
# Minor approximations
|
|
227
|
+
return "LOW"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Layout overflow analyzer — detects button count overflow and KEM incompatibility.
|
|
2
|
+
|
|
3
|
+
Sweeps CanonicalDeviceLayout objects cross-referenced with CanonicalDevice objects.
|
|
4
|
+
Detects phones where the CUCM button template assigns more line keys than the Webex
|
|
5
|
+
device model supports, or where KEM buttons exist but the Webex model doesn't support KEM.
|
|
6
|
+
|
|
7
|
+
Sources:
|
|
8
|
+
- tier2-phase2-phone-config-design.md §5.1
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from wxcli.migration.models import DecisionOption, DecisionType
|
|
17
|
+
from wxcli.migration.store import MigrationStore
|
|
18
|
+
from wxcli.migration.transform.analyzers import Analyzer, Decision
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Phone model → max line key count — imported from the single source of truth
|
|
23
|
+
from wxcli.migration.phone_models import MODEL_LINE_KEY_COUNTS as _MODEL_LINE_KEY_COUNTS
|
|
24
|
+
|
|
25
|
+
# Models that support KEM expansion modules
|
|
26
|
+
_KEM_SUPPORTED_MODELS: set[str] = {
|
|
27
|
+
"8845", "8851", "8861", "8865",
|
|
28
|
+
"9851", "9861",
|
|
29
|
+
"6851", "6861", "6871",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_model_number(model: str) -> str:
|
|
34
|
+
"""Extract numeric model identifier from full model string.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
"CP-8845" → "8845"
|
|
38
|
+
"Cisco 9841" → "9841"
|
|
39
|
+
"DMS Cisco 8845" → "8845"
|
|
40
|
+
"""
|
|
41
|
+
if not model:
|
|
42
|
+
return ""
|
|
43
|
+
# Strip common prefixes
|
|
44
|
+
cleaned = model.replace("CP-", "").replace("Cisco ", "").replace("DMS ", "")
|
|
45
|
+
# Take first token that looks like a model number (4 digits)
|
|
46
|
+
for token in cleaned.split():
|
|
47
|
+
digits = "".join(c for c in token if c.isdigit())
|
|
48
|
+
if len(digits) >= 4:
|
|
49
|
+
return digits
|
|
50
|
+
# Fallback: return cleaned string
|
|
51
|
+
return cleaned.strip()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LayoutOverflowAnalyzer(Analyzer):
|
|
55
|
+
"""Detects layout overflow (too many line keys) and KEM incompatibility.
|
|
56
|
+
|
|
57
|
+
Sweeps all CanonicalDeviceLayout objects and checks:
|
|
58
|
+
- resolved_line_keys count vs device model max_line_count
|
|
59
|
+
- resolved_kem_keys present but device model doesn't support KEM
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name = "layout_overflow"
|
|
63
|
+
decision_types = [DecisionType.FEATURE_APPROXIMATION]
|
|
64
|
+
depends_on: list[str] = []
|
|
65
|
+
|
|
66
|
+
def analyze(self, store: MigrationStore) -> list[Decision]:
|
|
67
|
+
decisions: list[Decision] = []
|
|
68
|
+
|
|
69
|
+
layouts = store.get_objects("device_layout")
|
|
70
|
+
if not layouts:
|
|
71
|
+
return decisions
|
|
72
|
+
|
|
73
|
+
for layout in layouts:
|
|
74
|
+
device_cid = layout.get("device_canonical_id", "")
|
|
75
|
+
if not device_cid:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
device = store.get_object(device_cid)
|
|
79
|
+
if device is None:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
model_raw = device.get("model", "")
|
|
83
|
+
model_num = _extract_model_number(model_raw)
|
|
84
|
+
|
|
85
|
+
resolved_keys = layout.get("resolved_line_keys", [])
|
|
86
|
+
kem_keys = layout.get("resolved_kem_keys", [])
|
|
87
|
+
layout_cid = layout.get("canonical_id", "")
|
|
88
|
+
|
|
89
|
+
# Check 1: Line key overflow
|
|
90
|
+
max_keys = _MODEL_LINE_KEY_COUNTS.get(model_num)
|
|
91
|
+
if max_keys is not None and len(resolved_keys) > max_keys:
|
|
92
|
+
context = {
|
|
93
|
+
"layout_canonical_id": layout_cid,
|
|
94
|
+
"device_canonical_id": device_cid,
|
|
95
|
+
"model": model_raw,
|
|
96
|
+
"model_number": model_num,
|
|
97
|
+
"max_line_keys": max_keys,
|
|
98
|
+
"actual_line_keys": len(resolved_keys),
|
|
99
|
+
"overflow_count": len(resolved_keys) - max_keys,
|
|
100
|
+
"overflow_type": "line_key_count",
|
|
101
|
+
}
|
|
102
|
+
options = [
|
|
103
|
+
DecisionOption(
|
|
104
|
+
id="accept",
|
|
105
|
+
label=f"Truncate to first {max_keys} keys",
|
|
106
|
+
impact=f"{len(resolved_keys) - max_keys} line keys will be dropped",
|
|
107
|
+
),
|
|
108
|
+
DecisionOption(
|
|
109
|
+
id="manual",
|
|
110
|
+
label="Create custom line key template",
|
|
111
|
+
impact="Manually assign line keys to fit device capacity",
|
|
112
|
+
),
|
|
113
|
+
DecisionOption(
|
|
114
|
+
id="skip",
|
|
115
|
+
label="Use Webex default layout",
|
|
116
|
+
impact="Phone will use Webex default line key assignment",
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
decisions.append(self._create_decision(
|
|
120
|
+
store=store,
|
|
121
|
+
decision_type=DecisionType.FEATURE_APPROXIMATION,
|
|
122
|
+
severity="MEDIUM",
|
|
123
|
+
summary=(
|
|
124
|
+
f"Line key overflow on {model_raw}: "
|
|
125
|
+
f"{len(resolved_keys)} keys exceeds max {max_keys}"
|
|
126
|
+
),
|
|
127
|
+
context=context,
|
|
128
|
+
options=options,
|
|
129
|
+
affected_objects=[layout_cid, device_cid],
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
# Check 2: KEM buttons on non-KEM device
|
|
133
|
+
if kem_keys and model_num not in _KEM_SUPPORTED_MODELS:
|
|
134
|
+
context = {
|
|
135
|
+
"layout_canonical_id": layout_cid,
|
|
136
|
+
"device_canonical_id": device_cid,
|
|
137
|
+
"model": model_raw,
|
|
138
|
+
"model_number": model_num,
|
|
139
|
+
"kem_key_count": len(kem_keys),
|
|
140
|
+
"overflow_type": "kem_not_supported",
|
|
141
|
+
}
|
|
142
|
+
options = [
|
|
143
|
+
DecisionOption(
|
|
144
|
+
id="accept_loss",
|
|
145
|
+
label="KEM buttons will be lost",
|
|
146
|
+
impact=f"{len(kem_keys)} KEM buttons cannot be migrated — device does not support KEM",
|
|
147
|
+
),
|
|
148
|
+
DecisionOption(
|
|
149
|
+
id="manual",
|
|
150
|
+
label="Attach compatible KEM to new device",
|
|
151
|
+
impact="Replace device with KEM-compatible model or add KEM module",
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
decisions.append(self._create_decision(
|
|
155
|
+
store=store,
|
|
156
|
+
decision_type=DecisionType.FEATURE_APPROXIMATION,
|
|
157
|
+
severity="MEDIUM",
|
|
158
|
+
summary=(
|
|
159
|
+
f"KEM buttons on {model_raw} which does not support KEM: "
|
|
160
|
+
f"{len(kem_keys)} KEM keys will be lost"
|
|
161
|
+
),
|
|
162
|
+
context=context,
|
|
163
|
+
options=options,
|
|
164
|
+
affected_objects=[layout_cid, device_cid],
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
return decisions
|
|
168
|
+
|
|
169
|
+
def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
|
|
170
|
+
return self._hash_fingerprint({
|
|
171
|
+
"device_canonical_id": context.get("device_canonical_id", ""),
|
|
172
|
+
"overflow_type": context.get("overflow_type", ""),
|
|
173
|
+
"model": context.get("model", ""),
|
|
174
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Location ambiguity analyzer — finds locations with ambiguous device pool mappings.
|
|
2
|
+
|
|
3
|
+
Mapper-owned decision type: location_mapper may already produce LOCATION_AMBIGUOUS
|
|
4
|
+
decisions. This analyzer backfills any locations the mapper missed or detects
|
|
5
|
+
cross-object ambiguities that emerge after the full inventory is mapped.
|
|
6
|
+
|
|
7
|
+
(from 03-conflict-detection-engine.md, location ambiguity linter)
|
|
8
|
+
(from phase-06-analyzers.md, Decision Ownership Table — mapper-owned)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from wxcli.migration.models import DecisionOption, DecisionType
|
|
16
|
+
from wxcli.migration.store import MigrationStore
|
|
17
|
+
from wxcli.migration.transform.analyzers import Analyzer, Decision
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LocationAmbiguityAnalyzer(Analyzer):
|
|
21
|
+
"""Detects locations with ambiguous CUCM device pool consolidation.
|
|
22
|
+
|
|
23
|
+
Sweeps all location objects and flags those whose ``warnings`` list
|
|
24
|
+
contains ambiguity indicators (e.g., multiple device pools consolidated
|
|
25
|
+
with conflicting CUCM location references).
|
|
26
|
+
|
|
27
|
+
Skips locations that already have a mapper-produced LOCATION_AMBIGUOUS decision.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name = "location_ambiguity"
|
|
31
|
+
decision_types = [DecisionType.LOCATION_AMBIGUOUS]
|
|
32
|
+
depends_on: list[str] = []
|
|
33
|
+
|
|
34
|
+
# Substrings in warnings that indicate location ambiguity
|
|
35
|
+
_AMBIGUITY_MARKERS = ("ambiguous", "conflicting", "multiple cucm locations", "mismatch")
|
|
36
|
+
|
|
37
|
+
def analyze(self, store: MigrationStore) -> list[Decision]:
|
|
38
|
+
decisions: list[Decision] = []
|
|
39
|
+
|
|
40
|
+
# Get existing mapper-produced decisions indexed by affected object canonical_id
|
|
41
|
+
existing_ambiguous = self._get_existing_decisions_for_type(
|
|
42
|
+
store, DecisionType.LOCATION_AMBIGUOUS
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
locations = store.get_objects("location")
|
|
46
|
+
|
|
47
|
+
for location in locations:
|
|
48
|
+
canonical_id = location.get("canonical_id", "")
|
|
49
|
+
|
|
50
|
+
# Skip if mapper already produced a LOCATION_AMBIGUOUS decision for this location
|
|
51
|
+
if canonical_id in existing_ambiguous:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
ambiguity_reasons: list[str] = []
|
|
55
|
+
|
|
56
|
+
# Check 1: warnings/errors containing ambiguity markers
|
|
57
|
+
warnings = location.get("warnings", [])
|
|
58
|
+
errors = location.get("errors", [])
|
|
59
|
+
all_messages = warnings + errors
|
|
60
|
+
|
|
61
|
+
for msg in all_messages:
|
|
62
|
+
if any(marker in msg.lower() for marker in self._AMBIGUITY_MARKERS):
|
|
63
|
+
ambiguity_reasons.append(msg)
|
|
64
|
+
|
|
65
|
+
# Check 2: multiple consolidated device pools (cross-object signal)
|
|
66
|
+
# A location consolidating 3+ device pools is inherently ambiguous —
|
|
67
|
+
# the mapper may not have flagged it if no CUCM location mismatch
|
|
68
|
+
# was detected, but the admin should still review the consolidation.
|
|
69
|
+
pool_names = location.get("cucm_device_pool_names", [])
|
|
70
|
+
if len(pool_names) >= 3 and not ambiguity_reasons:
|
|
71
|
+
ambiguity_reasons.append(
|
|
72
|
+
f"Location consolidates {len(pool_names)} device pools: "
|
|
73
|
+
f"{', '.join(pool_names[:5])}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not ambiguity_reasons:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
decisions.append(self._build_ambiguous_decision(
|
|
80
|
+
store, location, ambiguity_reasons
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
return decisions
|
|
84
|
+
|
|
85
|
+
def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
|
|
86
|
+
pool_names = context.get("cucm_device_pool_names", [])
|
|
87
|
+
return self._hash_fingerprint({
|
|
88
|
+
"canonical_id": context.get("canonical_id", ""),
|
|
89
|
+
"cucm_device_pool_names": sorted(pool_names),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Decision builder
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _build_ambiguous_decision(
|
|
97
|
+
self,
|
|
98
|
+
store: MigrationStore,
|
|
99
|
+
location: dict[str, Any],
|
|
100
|
+
ambiguity_warnings: list[str],
|
|
101
|
+
) -> Decision:
|
|
102
|
+
canonical_id = location.get("canonical_id", "")
|
|
103
|
+
name = location.get("name", "unknown")
|
|
104
|
+
pool_names = location.get("cucm_device_pool_names", [])
|
|
105
|
+
|
|
106
|
+
# Count devices + users depending on this location's device pools
|
|
107
|
+
dependent_count = 0
|
|
108
|
+
for pool_name in pool_names:
|
|
109
|
+
pool_id = f"device_pool:{pool_name}"
|
|
110
|
+
dependent_count += len(store.get_cross_refs(
|
|
111
|
+
to_id=pool_id, relationship="device_in_pool",
|
|
112
|
+
))
|
|
113
|
+
dependent_count += len(store.get_cross_refs(
|
|
114
|
+
to_id=pool_id, relationship="common_area_device_in_pool",
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
has_address = bool(
|
|
118
|
+
location.get("address", {}).get("address1")
|
|
119
|
+
if isinstance(location.get("address"), dict)
|
|
120
|
+
else None
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
context = {
|
|
124
|
+
"canonical_id": canonical_id,
|
|
125
|
+
"location_name": name,
|
|
126
|
+
"cucm_device_pool_names": pool_names,
|
|
127
|
+
"warnings": ambiguity_warnings,
|
|
128
|
+
"dependent_device_count": dependent_count,
|
|
129
|
+
"has_address": has_address,
|
|
130
|
+
}
|
|
131
|
+
options = [
|
|
132
|
+
DecisionOption(
|
|
133
|
+
id="accept",
|
|
134
|
+
label="Accept this location mapping",
|
|
135
|
+
impact=f"Location '{name}' will be created with consolidated device pools: "
|
|
136
|
+
f"{', '.join(pool_names) or 'none'}",
|
|
137
|
+
),
|
|
138
|
+
DecisionOption(
|
|
139
|
+
id="provide_address",
|
|
140
|
+
label="Provide address for this location",
|
|
141
|
+
impact=f"Location '{name}' needs a street address to create in Webex. "
|
|
142
|
+
f"Use 'wxcli cucm decide' to supply address1, city, state, postal_code, country.",
|
|
143
|
+
),
|
|
144
|
+
DecisionOption(
|
|
145
|
+
id="reassign",
|
|
146
|
+
label="Manually assign to different location",
|
|
147
|
+
impact=f"Device pools {', '.join(pool_names) or 'none'} will be "
|
|
148
|
+
f"reassigned to a user-specified location",
|
|
149
|
+
),
|
|
150
|
+
DecisionOption(
|
|
151
|
+
id="split",
|
|
152
|
+
label="Split into separate locations",
|
|
153
|
+
impact=f"Each device pool will become its own Webex location "
|
|
154
|
+
f"({len(pool_names)} locations)",
|
|
155
|
+
),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# Only offer skip if no devices depend on this location
|
|
159
|
+
if dependent_count == 0:
|
|
160
|
+
options.append(DecisionOption(
|
|
161
|
+
id="skip",
|
|
162
|
+
label="Skip this location (no devices affected)",
|
|
163
|
+
impact=f"Location '{name}' will not be migrated (0 devices in its pools)",
|
|
164
|
+
))
|
|
165
|
+
else:
|
|
166
|
+
# Make the impact of NOT having an address very clear
|
|
167
|
+
options[1] = DecisionOption(
|
|
168
|
+
id="provide_address",
|
|
169
|
+
label=f"Provide address for this location ({dependent_count} devices depend on it)",
|
|
170
|
+
impact=f"Location '{name}' has {dependent_count} devices in its pools. "
|
|
171
|
+
f"A street address is REQUIRED to create this location in Webex. "
|
|
172
|
+
f"Skipping is not available because it would block all {dependent_count} devices.",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return self._create_decision(
|
|
176
|
+
store=store,
|
|
177
|
+
decision_type=DecisionType.LOCATION_AMBIGUOUS,
|
|
178
|
+
severity="HIGH",
|
|
179
|
+
summary=f"Location '{name}' has ambiguous device pool mapping: "
|
|
180
|
+
f"{'; '.join(ambiguity_warnings[:2])}",
|
|
181
|
+
context=context,
|
|
182
|
+
options=options,
|
|
183
|
+
affected_objects=[canonical_id],
|
|
184
|
+
)
|