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,493 @@
|
|
|
1
|
+
"""Routing extractor — partitions, CSS, route patterns, gateways,
|
|
2
|
+
SIP trunks, route groups, route lists, and translation patterns.
|
|
3
|
+
|
|
4
|
+
Covers 8 object types that define CUCM call routing topology.
|
|
5
|
+
|
|
6
|
+
Sources:
|
|
7
|
+
- 02b-cucm-extraction.md §2.4 (routing extraction, all 8 types)
|
|
8
|
+
- 02b-cucm-extraction.md §3 (base extractor, pagination)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
17
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
18
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
# ReturnedTags constants — one per AXL object type (from 02b §2.4)
|
|
24
|
+
# ------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
PARTITION_RETURNED_TAGS = {'name': '', 'description': ''}
|
|
27
|
+
|
|
28
|
+
# Verified: listCss does NOT accept 'members' — only getCss returns members.
|
|
29
|
+
# List with name+description only, then getCss for full member ordering.
|
|
30
|
+
CSS_LIST_RETURNED_TAGS = {'name': '', 'description': ''}
|
|
31
|
+
|
|
32
|
+
# Verified against live CUCM 15.0 (2026-03-23):
|
|
33
|
+
# - 'destination' is NOT a valid listRoutePattern returnedTag; route targets
|
|
34
|
+
# require getRoutePattern for full resolution
|
|
35
|
+
# - 'networkLocation' and 'prefixDigitsOut' confirmed present in list response
|
|
36
|
+
ROUTE_PATTERN_RETURNED_TAGS = {
|
|
37
|
+
'pattern': '', 'routePartitionName': '',
|
|
38
|
+
'blockEnable': '', 'description': '', 'calledPartyTransformationMask': '',
|
|
39
|
+
'callingPartyTransformationMask': '', 'prefixDigitsOut': '', 'networkLocation': '',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Verified: listGateway does NOT return devicePoolName — only domainName,
|
|
43
|
+
# description, product, protocol. Use getGateway for devicePoolName.
|
|
44
|
+
GATEWAY_RETURNED_TAGS = {
|
|
45
|
+
'domainName': '', 'description': '', 'product': '', 'protocol': '',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Verified against live CUCM 15.0 (2026-03-23):
|
|
49
|
+
# - listSipTrunk does NOT accept 'destinations' — only getSipTrunk returns those
|
|
50
|
+
# - getSipTrunk returns destinations.destination[].addressIpv4, .port, .sortOrder
|
|
51
|
+
# - sipTrunkType returns values like 'None(Default)'
|
|
52
|
+
SIP_TRUNK_LIST_RETURNED_TAGS = {
|
|
53
|
+
'name': '', 'description': '', 'devicePoolName': '',
|
|
54
|
+
'sipProfileName': '', 'securityProfileName': '',
|
|
55
|
+
'sipTrunkType': '',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Verified: list schemas are restrictive. Use minimal tags for discovery, get for detail.
|
|
59
|
+
ROUTE_GROUP_LIST_RETURNED_TAGS = {'name': ''}
|
|
60
|
+
ROUTE_LIST_LIST_RETURNED_TAGS = {'name': ''}
|
|
61
|
+
|
|
62
|
+
TRANSLATION_PATTERN_RETURNED_TAGS = {
|
|
63
|
+
'pattern': '', 'calledPartyTransformationMask': '', 'description': '',
|
|
64
|
+
'routePartitionName': '',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RoutingExtractor(BaseExtractor):
|
|
69
|
+
"""Extract CUCM routing configuration — 8 object types.
|
|
70
|
+
|
|
71
|
+
(from 02b §2.4)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name = "routing"
|
|
75
|
+
|
|
76
|
+
def __init__(self, connection: AXLConnection) -> None:
|
|
77
|
+
super().__init__(connection)
|
|
78
|
+
self.results: dict[str, list[dict[str, Any]]] = {}
|
|
79
|
+
|
|
80
|
+
def extract(self) -> ExtractionResult:
|
|
81
|
+
"""Extract all 8 routing object types and aggregate results."""
|
|
82
|
+
result = ExtractionResult(extractor=self.name)
|
|
83
|
+
|
|
84
|
+
# Each sub-method populates self.results[key] and returns (count, errors)
|
|
85
|
+
extractors = [
|
|
86
|
+
("partitions", self._extract_partitions),
|
|
87
|
+
("css_list", self._extract_css),
|
|
88
|
+
("route_patterns", self._extract_route_patterns),
|
|
89
|
+
("gateways", self._extract_gateways),
|
|
90
|
+
("sip_trunks", self._extract_sip_trunks),
|
|
91
|
+
("route_groups", self._extract_route_groups),
|
|
92
|
+
("route_lists", self._extract_route_lists),
|
|
93
|
+
("translation_patterns", self._extract_translation_patterns),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
for key, extract_fn in extractors:
|
|
97
|
+
count, errors = extract_fn()
|
|
98
|
+
result.total += count
|
|
99
|
+
result.failed += len(errors)
|
|
100
|
+
result.errors.extend(errors)
|
|
101
|
+
|
|
102
|
+
logger.info(
|
|
103
|
+
"[%s] Extracted %d total objects (%d failed)",
|
|
104
|
+
self.name, result.success_count, result.failed,
|
|
105
|
+
)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# Partitions — list only (simple objects)
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _extract_partitions(self) -> tuple[int, list[str]]:
|
|
113
|
+
logger.info("[%s] Listing partitions...", self.name)
|
|
114
|
+
try:
|
|
115
|
+
partitions = self.paginated_list(
|
|
116
|
+
method_name="listRoutePartition",
|
|
117
|
+
search_criteria={"name": "%"},
|
|
118
|
+
returned_tags=PARTITION_RETURNED_TAGS,
|
|
119
|
+
)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
msg = f"listRoutePartition failed: {exc}"
|
|
122
|
+
logger.error("[%s] %s", self.name, msg)
|
|
123
|
+
self.results["partitions"] = []
|
|
124
|
+
return 0, [msg]
|
|
125
|
+
self.results["partitions"] = partitions
|
|
126
|
+
logger.info("[%s] Found %d partitions", self.name, len(partitions))
|
|
127
|
+
return len(partitions), []
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
# CSS — list + get for full ordered members (CRITICAL: preserve ordering)
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _extract_css(self) -> tuple[int, list[str]]:
|
|
134
|
+
logger.info("[%s] Listing calling search spaces...", self.name)
|
|
135
|
+
try:
|
|
136
|
+
css_summaries = self.paginated_list(
|
|
137
|
+
method_name="listCss",
|
|
138
|
+
search_criteria={"name": "%"},
|
|
139
|
+
returned_tags=CSS_LIST_RETURNED_TAGS,
|
|
140
|
+
)
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
msg = f"listCss failed: {exc}"
|
|
143
|
+
logger.error("[%s] %s", self.name, msg)
|
|
144
|
+
self.results["css_list"] = []
|
|
145
|
+
return 0, [msg]
|
|
146
|
+
logger.info("[%s] Found %d CSS", self.name, len(css_summaries))
|
|
147
|
+
|
|
148
|
+
css_list: list[dict[str, Any]] = []
|
|
149
|
+
errors: list[str] = []
|
|
150
|
+
for summary in css_summaries:
|
|
151
|
+
css_name = ref_value(summary.get("name")) or summary.get("name")
|
|
152
|
+
if not css_name:
|
|
153
|
+
errors.append("CSS with no name in listCss result")
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
detail = self._get_css_detail(css_name)
|
|
157
|
+
if detail is None:
|
|
158
|
+
errors.append(f"getCss failed for {css_name}")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
css_list.append(detail)
|
|
162
|
+
|
|
163
|
+
self.results["css_list"] = css_list
|
|
164
|
+
return len(css_summaries), errors
|
|
165
|
+
|
|
166
|
+
def _get_css_detail(self, css_name: str) -> dict[str, Any] | None:
|
|
167
|
+
"""Fetch full CSS detail and normalize member ordering.
|
|
168
|
+
|
|
169
|
+
CRITICAL: CSS member ordering determines partition priority.
|
|
170
|
+
The ``index`` field on each member defines the evaluation order.
|
|
171
|
+
Members are sorted by index to preserve correct ordering.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
detail = self.get_detail("getCss", name=css_name)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
logger.warning("[%s] getCss error for %s: %s", self.name, css_name, exc)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
if detail is None:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Normalize members — may be None, empty dict, or populated
|
|
183
|
+
raw_members = detail.get("members")
|
|
184
|
+
member_entries = to_list(raw_members, "member")
|
|
185
|
+
|
|
186
|
+
# Sort by index to preserve partition evaluation order
|
|
187
|
+
sorted_members = sorted(
|
|
188
|
+
member_entries,
|
|
189
|
+
key=lambda m: int(m.get("index", 0)) if isinstance(m, dict) else 0,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
detail["members"] = sorted_members
|
|
193
|
+
return detail
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Route Patterns — list + get (destination requires getRoutePattern)
|
|
197
|
+
# Verified: listRoutePattern does NOT return destination/gateway/routeList
|
|
198
|
+
# fields. getRoutePattern returns the full routing target.
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def _extract_route_patterns(self) -> tuple[int, list[str]]:
|
|
202
|
+
logger.info("[%s] Listing route patterns...", self.name)
|
|
203
|
+
try:
|
|
204
|
+
rp_summaries = self.paginated_list(
|
|
205
|
+
method_name="listRoutePattern",
|
|
206
|
+
search_criteria={"pattern": "%"},
|
|
207
|
+
returned_tags=ROUTE_PATTERN_RETURNED_TAGS,
|
|
208
|
+
)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
msg = f"listRoutePattern failed: {exc}"
|
|
211
|
+
logger.error("[%s] %s", self.name, msg)
|
|
212
|
+
self.results["route_patterns"] = []
|
|
213
|
+
return 0, [msg]
|
|
214
|
+
logger.info("[%s] Found %d route patterns", self.name, len(rp_summaries))
|
|
215
|
+
|
|
216
|
+
patterns: list[dict[str, Any]] = []
|
|
217
|
+
errors: list[str] = []
|
|
218
|
+
for summary in rp_summaries:
|
|
219
|
+
rp_pattern = summary.get("pattern", "")
|
|
220
|
+
rp_partition = ref_value(summary.get("routePartitionName")) or summary.get("routePartitionName")
|
|
221
|
+
try:
|
|
222
|
+
detail = self.get_detail(
|
|
223
|
+
"getRoutePattern",
|
|
224
|
+
pattern=rp_pattern,
|
|
225
|
+
routePartitionName=rp_partition or "",
|
|
226
|
+
)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
logger.warning(
|
|
229
|
+
"[%s] getRoutePattern error for %s:%s: %s",
|
|
230
|
+
self.name, rp_pattern, rp_partition, exc,
|
|
231
|
+
)
|
|
232
|
+
errors.append(f"getRoutePattern failed for {rp_pattern}:{rp_partition}")
|
|
233
|
+
patterns.append(summary)
|
|
234
|
+
continue
|
|
235
|
+
if detail is None:
|
|
236
|
+
patterns.append(summary)
|
|
237
|
+
continue
|
|
238
|
+
patterns.append(detail)
|
|
239
|
+
|
|
240
|
+
self.results["route_patterns"] = patterns
|
|
241
|
+
return len(rp_summaries), errors
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
# Gateways — list + get (devicePoolName requires getGateway)
|
|
245
|
+
# Verified: listGateway search criteria is domainName
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def _extract_gateways(self) -> tuple[int, list[str]]:
|
|
249
|
+
logger.info("[%s] Listing gateways...", self.name)
|
|
250
|
+
try:
|
|
251
|
+
gw_summaries = self.paginated_list(
|
|
252
|
+
method_name="listGateway",
|
|
253
|
+
search_criteria={"domainName": "%"},
|
|
254
|
+
returned_tags=GATEWAY_RETURNED_TAGS,
|
|
255
|
+
)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
msg = f"listGateway failed: {exc}"
|
|
258
|
+
logger.error("[%s] %s", self.name, msg)
|
|
259
|
+
self.results["gateways"] = []
|
|
260
|
+
return 0, [msg]
|
|
261
|
+
logger.info("[%s] Found %d gateways", self.name, len(gw_summaries))
|
|
262
|
+
|
|
263
|
+
gateways: list[dict[str, Any]] = []
|
|
264
|
+
errors: list[str] = []
|
|
265
|
+
for summary in gw_summaries:
|
|
266
|
+
gw_name = ref_value(summary.get("domainName")) or summary.get("domainName")
|
|
267
|
+
if not gw_name:
|
|
268
|
+
errors.append("Gateway with no domainName in listGateway result")
|
|
269
|
+
continue
|
|
270
|
+
try:
|
|
271
|
+
detail = self.get_detail("getGateway", domainName=gw_name)
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
logger.warning("[%s] getGateway error for %s: %s", self.name, gw_name, exc)
|
|
274
|
+
errors.append(f"getGateway failed for {gw_name}")
|
|
275
|
+
# Fall back to summary (no devicePoolName)
|
|
276
|
+
gateways.append(summary)
|
|
277
|
+
continue
|
|
278
|
+
if detail is None:
|
|
279
|
+
gateways.append(summary)
|
|
280
|
+
continue
|
|
281
|
+
gateways.append(detail)
|
|
282
|
+
|
|
283
|
+
self.results["gateways"] = gateways
|
|
284
|
+
return len(gw_summaries), errors
|
|
285
|
+
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
# SIP Trunks — list + get (destinations require getSipTrunk)
|
|
288
|
+
# Verified: listSipTrunk method confirmed on live CUCM 15.0
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
def _extract_sip_trunks(self) -> tuple[int, list[str]]:
|
|
292
|
+
logger.info("[%s] Listing SIP trunks...", self.name)
|
|
293
|
+
try:
|
|
294
|
+
trunk_summaries = self.paginated_list(
|
|
295
|
+
method_name="listSipTrunk",
|
|
296
|
+
search_criteria={"name": "%"},
|
|
297
|
+
returned_tags=SIP_TRUNK_LIST_RETURNED_TAGS,
|
|
298
|
+
)
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
msg = f"listSipTrunk failed: {exc}"
|
|
301
|
+
logger.error("[%s] %s", self.name, msg)
|
|
302
|
+
self.results["sip_trunks"] = []
|
|
303
|
+
return 0, [msg]
|
|
304
|
+
logger.info("[%s] Found %d SIP trunks", self.name, len(trunk_summaries))
|
|
305
|
+
|
|
306
|
+
# getSipTrunk for full detail including destinations array
|
|
307
|
+
trunks: list[dict[str, Any]] = []
|
|
308
|
+
errors: list[str] = []
|
|
309
|
+
for summary in trunk_summaries:
|
|
310
|
+
trunk_name = ref_value(summary.get("name")) or summary.get("name")
|
|
311
|
+
if not trunk_name:
|
|
312
|
+
errors.append("SIP trunk with no name in listSipTrunk result")
|
|
313
|
+
continue
|
|
314
|
+
try:
|
|
315
|
+
detail = self.get_detail("getSipTrunk", name=trunk_name)
|
|
316
|
+
except Exception as exc:
|
|
317
|
+
logger.warning("[%s] getSipTrunk error for %s: %s", self.name, trunk_name, exc)
|
|
318
|
+
errors.append(f"getSipTrunk failed for {trunk_name}")
|
|
319
|
+
trunks.append(summary)
|
|
320
|
+
continue
|
|
321
|
+
if detail is None:
|
|
322
|
+
trunks.append(summary)
|
|
323
|
+
continue
|
|
324
|
+
# Normalize destinations list
|
|
325
|
+
raw_dest = detail.get("destinations") if hasattr(detail, "get") else None
|
|
326
|
+
if raw_dest:
|
|
327
|
+
detail["destinations"] = to_list(raw_dest, "destination")
|
|
328
|
+
trunks.append(detail)
|
|
329
|
+
|
|
330
|
+
# Second pass: fetch SIP profile and security profile detail
|
|
331
|
+
# Collect unique profile names across all trunks
|
|
332
|
+
sip_profile_names: set[str] = set()
|
|
333
|
+
security_profile_names: set[str] = set()
|
|
334
|
+
for trunk in trunks:
|
|
335
|
+
sip_name = ref_value(trunk.get("sipProfileName")) or trunk.get("sipProfileName")
|
|
336
|
+
if sip_name and isinstance(sip_name, str):
|
|
337
|
+
sip_profile_names.add(sip_name)
|
|
338
|
+
sec_name = ref_value(trunk.get("securityProfileName")) or trunk.get("securityProfileName")
|
|
339
|
+
if sec_name and isinstance(sec_name, str):
|
|
340
|
+
security_profile_names.add(sec_name)
|
|
341
|
+
|
|
342
|
+
# Fetch SIP profiles (cache to avoid duplicate calls)
|
|
343
|
+
sip_profile_cache: dict[str, dict[str, Any]] = {}
|
|
344
|
+
for profile_name in sip_profile_names:
|
|
345
|
+
try:
|
|
346
|
+
detail = self.get_detail("getSipProfile", name=profile_name)
|
|
347
|
+
if detail is not None:
|
|
348
|
+
sip_profile_cache[profile_name] = detail
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
logger.warning(
|
|
351
|
+
"[%s] getSipProfile error for %s: %s", self.name, profile_name, exc,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Fetch security profiles (cache to avoid duplicate calls)
|
|
355
|
+
security_profile_cache: dict[str, dict[str, Any]] = {}
|
|
356
|
+
for profile_name in security_profile_names:
|
|
357
|
+
try:
|
|
358
|
+
detail = self.get_detail("getSipTrunkSecurityProfile", name=profile_name)
|
|
359
|
+
if detail is not None:
|
|
360
|
+
security_profile_cache[profile_name] = detail
|
|
361
|
+
except Exception as exc:
|
|
362
|
+
logger.warning(
|
|
363
|
+
"[%s] getSipTrunkSecurityProfile error for %s: %s", self.name, profile_name, exc,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Merge profile detail into each trunk dict
|
|
367
|
+
for trunk in trunks:
|
|
368
|
+
sip_name = ref_value(trunk.get("sipProfileName")) or trunk.get("sipProfileName")
|
|
369
|
+
if sip_name and sip_name in sip_profile_cache:
|
|
370
|
+
trunk["_sip_profile_detail"] = sip_profile_cache[sip_name]
|
|
371
|
+
sec_name = ref_value(trunk.get("securityProfileName")) or trunk.get("securityProfileName")
|
|
372
|
+
if sec_name and sec_name in security_profile_cache:
|
|
373
|
+
trunk["_security_profile_detail"] = security_profile_cache[sec_name]
|
|
374
|
+
|
|
375
|
+
self.results["sip_trunks"] = trunks
|
|
376
|
+
return len(trunk_summaries), errors
|
|
377
|
+
|
|
378
|
+
# ------------------------------------------------------------------
|
|
379
|
+
# Route Groups — list + get for member trunks
|
|
380
|
+
# ------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
def _extract_route_groups(self) -> tuple[int, list[str]]:
|
|
383
|
+
logger.info("[%s] Listing route groups...", self.name)
|
|
384
|
+
try:
|
|
385
|
+
rg_summaries = self.paginated_list(
|
|
386
|
+
method_name="listRouteGroup",
|
|
387
|
+
search_criteria={"name": "%"},
|
|
388
|
+
returned_tags=ROUTE_GROUP_LIST_RETURNED_TAGS,
|
|
389
|
+
)
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
msg = f"listRouteGroup failed: {exc}"
|
|
392
|
+
logger.error("[%s] %s", self.name, msg)
|
|
393
|
+
self.results["route_groups"] = []
|
|
394
|
+
return 0, [msg]
|
|
395
|
+
logger.info("[%s] Found %d route groups", self.name, len(rg_summaries))
|
|
396
|
+
|
|
397
|
+
route_groups: list[dict[str, Any]] = []
|
|
398
|
+
errors: list[str] = []
|
|
399
|
+
for summary in rg_summaries:
|
|
400
|
+
rg_name = ref_value(summary.get("name")) or summary.get("name")
|
|
401
|
+
if not rg_name:
|
|
402
|
+
errors.append("Route group with no name in listRouteGroup result")
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
detail = self.get_detail("getRouteGroup", name=rg_name)
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
logger.warning(
|
|
409
|
+
"[%s] getRouteGroup error for %s: %s", self.name, rg_name, exc,
|
|
410
|
+
)
|
|
411
|
+
errors.append(f"getRouteGroup failed for {rg_name}")
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
if detail is None:
|
|
415
|
+
errors.append(f"getRouteGroup returned None for {rg_name}")
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
# Normalize members
|
|
419
|
+
raw_members = detail.get("members")
|
|
420
|
+
detail["members"] = to_list(raw_members, "member")
|
|
421
|
+
route_groups.append(detail)
|
|
422
|
+
|
|
423
|
+
self.results["route_groups"] = route_groups
|
|
424
|
+
return len(rg_summaries), errors
|
|
425
|
+
|
|
426
|
+
# ------------------------------------------------------------------
|
|
427
|
+
# Route Lists — list + get for member route groups
|
|
428
|
+
# ------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
def _extract_route_lists(self) -> tuple[int, list[str]]:
|
|
431
|
+
logger.info("[%s] Listing route lists...", self.name)
|
|
432
|
+
try:
|
|
433
|
+
rl_summaries = self.paginated_list(
|
|
434
|
+
method_name="listRouteList",
|
|
435
|
+
search_criteria={"name": "%"},
|
|
436
|
+
returned_tags=ROUTE_LIST_LIST_RETURNED_TAGS,
|
|
437
|
+
)
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
msg = f"listRouteList failed: {exc}"
|
|
440
|
+
logger.error("[%s] %s", self.name, msg)
|
|
441
|
+
self.results["route_lists"] = []
|
|
442
|
+
return 0, [msg]
|
|
443
|
+
logger.info("[%s] Found %d route lists", self.name, len(rl_summaries))
|
|
444
|
+
|
|
445
|
+
route_lists: list[dict[str, Any]] = []
|
|
446
|
+
errors: list[str] = []
|
|
447
|
+
for summary in rl_summaries:
|
|
448
|
+
rl_name = ref_value(summary.get("name")) or summary.get("name")
|
|
449
|
+
if not rl_name:
|
|
450
|
+
errors.append("Route list with no name in listRouteList result")
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
detail = self.get_detail("getRouteList", name=rl_name)
|
|
455
|
+
except Exception as exc:
|
|
456
|
+
logger.warning(
|
|
457
|
+
"[%s] getRouteList error for %s: %s", self.name, rl_name, exc,
|
|
458
|
+
)
|
|
459
|
+
errors.append(f"getRouteList failed for {rl_name}")
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
if detail is None:
|
|
463
|
+
errors.append(f"getRouteList returned None for {rl_name}")
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
# Normalize members
|
|
467
|
+
raw_members = detail.get("members")
|
|
468
|
+
detail["members"] = to_list(raw_members, "member")
|
|
469
|
+
route_lists.append(detail)
|
|
470
|
+
|
|
471
|
+
self.results["route_lists"] = route_lists
|
|
472
|
+
return len(rl_summaries), errors
|
|
473
|
+
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
# Translation Patterns — list only (no get needed)
|
|
476
|
+
# ------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
def _extract_translation_patterns(self) -> tuple[int, list[str]]:
|
|
479
|
+
logger.info("[%s] Listing translation patterns...", self.name)
|
|
480
|
+
try:
|
|
481
|
+
patterns = self.paginated_list(
|
|
482
|
+
method_name="listTransPattern",
|
|
483
|
+
search_criteria={"pattern": "%"},
|
|
484
|
+
returned_tags=TRANSLATION_PATTERN_RETURNED_TAGS,
|
|
485
|
+
)
|
|
486
|
+
except Exception as exc:
|
|
487
|
+
msg = f"listTransPattern failed: {exc}"
|
|
488
|
+
logger.error("[%s] %s", self.name, msg)
|
|
489
|
+
self.results["translation_patterns"] = []
|
|
490
|
+
return 0, [msg]
|
|
491
|
+
self.results["translation_patterns"] = patterns
|
|
492
|
+
logger.info("[%s] Found %d translation patterns", self.name, len(patterns))
|
|
493
|
+
return len(patterns), []
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Shared line detection post-processor.
|
|
2
|
+
|
|
3
|
+
Identifies directory numbers that appear on two or more devices, indicating
|
|
4
|
+
shared-line appearances. Runs as a pass 2 helper against already-extracted
|
|
5
|
+
phone data -- does NOT make AXL calls.
|
|
6
|
+
|
|
7
|
+
Sources:
|
|
8
|
+
- 02b-cucm-extraction.md S2.7 (shared line detection)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SharedLineDetector:
|
|
23
|
+
"""Detect shared lines from DeviceExtractor phone data.
|
|
24
|
+
|
|
25
|
+
This is a pass 2 helper, NOT a BaseExtractor subclass. It operates
|
|
26
|
+
entirely on in-memory phone dicts and produces shared line records.
|
|
27
|
+
|
|
28
|
+
(from 02b S2.7)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, phones: list[dict]) -> None:
|
|
32
|
+
self.phones = phones
|
|
33
|
+
|
|
34
|
+
def detect(self) -> list[dict[str, Any]]:
|
|
35
|
+
"""Identify all shared lines and return structured dicts.
|
|
36
|
+
|
|
37
|
+
Algorithm:
|
|
38
|
+
1. Walk every phone's line appearances.
|
|
39
|
+
2. Group by (dn_pattern, partition).
|
|
40
|
+
3. Any DN appearing on 2+ devices is shared.
|
|
41
|
+
4. Build a shared-line dict for each.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of shared line dicts with canonical_id, devices, and
|
|
45
|
+
primary_owner information.
|
|
46
|
+
"""
|
|
47
|
+
# Map (dn_pattern, partition) -> list of appearance records
|
|
48
|
+
dn_appearances: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
|
49
|
+
|
|
50
|
+
for phone in self.phones:
|
|
51
|
+
device_name = phone.get("name", "")
|
|
52
|
+
owner = ref_value(phone.get("ownerUserName"))
|
|
53
|
+
|
|
54
|
+
lines_container = phone.get("lines")
|
|
55
|
+
if not lines_container:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
line_entries = to_list(lines_container, "line")
|
|
59
|
+
|
|
60
|
+
for line_entry in line_entries:
|
|
61
|
+
# Speed dials have no dirn — skip them
|
|
62
|
+
dirn = line_entry.get("dirn")
|
|
63
|
+
if dirn is None:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
dn_pattern = ref_value(dirn.get("pattern")) if isinstance(dirn, dict) else None
|
|
67
|
+
if not dn_pattern:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
partition = ""
|
|
71
|
+
route_partition = dirn.get("routePartitionName") if isinstance(dirn, dict) else None
|
|
72
|
+
if route_partition is not None:
|
|
73
|
+
partition = ref_value(route_partition) or ""
|
|
74
|
+
|
|
75
|
+
line_index_raw = line_entry.get("index")
|
|
76
|
+
try:
|
|
77
|
+
line_index = int(line_index_raw) if line_index_raw is not None else None
|
|
78
|
+
except (ValueError, TypeError):
|
|
79
|
+
line_index = None
|
|
80
|
+
|
|
81
|
+
# Collect associated endusers for this line appearance
|
|
82
|
+
assoc_endusers_container = line_entry.get("associatedEndusers")
|
|
83
|
+
assoc_endusers: list[str] = []
|
|
84
|
+
if assoc_endusers_container is not None:
|
|
85
|
+
enduser_entries = to_list(assoc_endusers_container, "enduser")
|
|
86
|
+
for eu in enduser_entries:
|
|
87
|
+
eu_id = ref_value(eu.get("userId")) if isinstance(eu, dict) else ref_value(eu)
|
|
88
|
+
if eu_id:
|
|
89
|
+
assoc_endusers.append(eu_id)
|
|
90
|
+
|
|
91
|
+
dn_appearances[(dn_pattern, partition)].append(
|
|
92
|
+
{
|
|
93
|
+
"device_name": device_name,
|
|
94
|
+
"device_owner": owner,
|
|
95
|
+
"line_index": line_index,
|
|
96
|
+
"associated_endusers": assoc_endusers,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Filter to DNs on 2+ devices and build output
|
|
101
|
+
results: list[dict[str, Any]] = []
|
|
102
|
+
|
|
103
|
+
for (dn_pattern, partition), appearances in sorted(dn_appearances.items()):
|
|
104
|
+
if len(appearances) < 2:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Determine primary owner: user with DN at line index 1.
|
|
108
|
+
# If multiple at index 1, first one wins.
|
|
109
|
+
primary_owner: str | None = None
|
|
110
|
+
for app in appearances:
|
|
111
|
+
if app["line_index"] == 1 and app["device_owner"] is not None:
|
|
112
|
+
primary_owner = app["device_owner"]
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
# Aggregate all associated endusers across devices (deduplicated)
|
|
116
|
+
all_endusers: list[str] = []
|
|
117
|
+
seen_endusers: set[str] = set()
|
|
118
|
+
for app in appearances:
|
|
119
|
+
for eu in app["associated_endusers"]:
|
|
120
|
+
if eu not in seen_endusers:
|
|
121
|
+
seen_endusers.add(eu)
|
|
122
|
+
all_endusers.append(eu)
|
|
123
|
+
|
|
124
|
+
devices = [
|
|
125
|
+
{
|
|
126
|
+
"device_name": app["device_name"],
|
|
127
|
+
"device_owner": app["device_owner"],
|
|
128
|
+
"line_index": app["line_index"],
|
|
129
|
+
}
|
|
130
|
+
for app in appearances
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
results.append(
|
|
134
|
+
{
|
|
135
|
+
"canonical_id": f"shared_line:{dn_pattern}:{partition}",
|
|
136
|
+
"dn": dn_pattern,
|
|
137
|
+
"partition": partition,
|
|
138
|
+
"device_count": len(appearances),
|
|
139
|
+
"devices": devices,
|
|
140
|
+
"primary_owner": primary_owner,
|
|
141
|
+
"associated_endusers": all_endusers,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
logger.info("Shared line detection complete: %d shared DNs found", len(results))
|
|
146
|
+
return results
|