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,229 @@
|
|
|
1
|
+
"""Template extractor — phone button templates and softkey templates.
|
|
2
|
+
|
|
3
|
+
Phone button templates use two-step AXL extraction (list → get-detail).
|
|
4
|
+
Softkey templates use SQL queries (AXL list/get operations don't exist in v15.0).
|
|
5
|
+
|
|
6
|
+
Templates are shared objects — typically 5-20 button templates and
|
|
7
|
+
3-10 softkey templates per cluster.
|
|
8
|
+
|
|
9
|
+
AXL gotchas (verified 2026-03-25 against CUCM 15.0):
|
|
10
|
+
- listPhoneButtonTemplate returnedTags only supports {name, isUserModifiable},
|
|
11
|
+
NOT basePhoneTemplateName.
|
|
12
|
+
- getPhoneButtonTemplate returns buttons with <buttonNumber> (not <index>),
|
|
13
|
+
and "Speed Dial BLF" as the feature name (not "Busy Lamp Field" as in add).
|
|
14
|
+
- addSoftkeyTemplate, getSoftkeyTemplate, listSoftkeyTemplate DO NOT EXIST.
|
|
15
|
+
Softkey templates must be queried via executeSQLQuery on the softkeytemplate
|
|
16
|
+
table, with typesoftkey for ID-to-name decoding.
|
|
17
|
+
|
|
18
|
+
Sources:
|
|
19
|
+
- tier2-phase2-phone-config-design.md §1.1, §13.1, §13.2
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
28
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
29
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# listPhoneButtonTemplate only supports these returnedTags (verified 2026-03-25)
|
|
34
|
+
BUTTON_TEMPLATE_LIST_TAGS = {"name": "", "isUserModifiable": ""}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Softkey ID → name mapping, populated once from typesoftkey SQL table
|
|
38
|
+
_SOFTKEY_ID_CACHE: dict[str, str] = {}
|
|
39
|
+
|
|
40
|
+
# softkeysetclause state positions (verified 2026-03-25, CUCM 15.0)
|
|
41
|
+
# Semicolon-delimited, 12 states in fixed order:
|
|
42
|
+
SOFTKEY_STATE_ORDER = [
|
|
43
|
+
"On Hook",
|
|
44
|
+
"Connected",
|
|
45
|
+
"On Hold",
|
|
46
|
+
"Ring In",
|
|
47
|
+
"Off Hook",
|
|
48
|
+
"Connected Transfer",
|
|
49
|
+
"Transition",
|
|
50
|
+
"Connected Conference",
|
|
51
|
+
"Park",
|
|
52
|
+
"Alerting",
|
|
53
|
+
"Shared Active",
|
|
54
|
+
"Shared Held",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TemplateExtractor(BaseExtractor):
|
|
59
|
+
"""Extract phone button templates and softkey templates from CUCM."""
|
|
60
|
+
|
|
61
|
+
name = "templates"
|
|
62
|
+
|
|
63
|
+
def __init__(self, connection: AXLConnection) -> None:
|
|
64
|
+
super().__init__(connection)
|
|
65
|
+
self.results: dict[str, list[dict[str, Any]]] = {}
|
|
66
|
+
|
|
67
|
+
def extract(self) -> ExtractionResult:
|
|
68
|
+
"""Run extraction for button templates (AXL) and softkey templates (SQL)."""
|
|
69
|
+
result = ExtractionResult(extractor=self.name)
|
|
70
|
+
|
|
71
|
+
button_templates = self._extract_button_templates(result)
|
|
72
|
+
softkey_templates = self._extract_softkey_templates(result)
|
|
73
|
+
|
|
74
|
+
self.results["button_templates"] = button_templates
|
|
75
|
+
self.results["softkey_templates"] = softkey_templates
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
"[%s] Extracted %d button templates, %d softkey templates (%d failed)",
|
|
79
|
+
self.name, len(button_templates), len(softkey_templates), result.failed,
|
|
80
|
+
)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Phone button templates — AXL list + get-detail
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _extract_button_templates(self, result: ExtractionResult) -> list[dict[str, Any]]:
|
|
88
|
+
"""List and get-detail all phone button templates."""
|
|
89
|
+
logger.info("[%s] Listing phone button templates...", self.name)
|
|
90
|
+
try:
|
|
91
|
+
summaries = self.paginated_list(
|
|
92
|
+
method_name="listPhoneButtonTemplate",
|
|
93
|
+
search_criteria={"name": "%"},
|
|
94
|
+
returned_tags=BUTTON_TEMPLATE_LIST_TAGS,
|
|
95
|
+
)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning("[%s] listPhoneButtonTemplate failed: %s", self.name, exc)
|
|
98
|
+
result.errors.append(f"listPhoneButtonTemplate failed: {exc}")
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
result.total += len(summaries)
|
|
102
|
+
|
|
103
|
+
templates: list[dict[str, Any]] = []
|
|
104
|
+
for summary in summaries:
|
|
105
|
+
name = ref_value(summary.get("name")) or summary.get("name")
|
|
106
|
+
if not name:
|
|
107
|
+
result.failed += 1
|
|
108
|
+
result.errors.append("Button template with no name")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
detail = self.get_detail("getPhoneButtonTemplate", name=name)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.warning("[%s] getPhoneButtonTemplate(%s) failed: %s", self.name, name, exc)
|
|
115
|
+
result.failed += 1
|
|
116
|
+
result.errors.append(f"getPhoneButtonTemplate failed for {name}")
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if detail is None:
|
|
120
|
+
result.failed += 1
|
|
121
|
+
result.errors.append(f"getPhoneButtonTemplate returned None for {name}")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
templates.append(detail)
|
|
125
|
+
|
|
126
|
+
return templates
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
# Softkey templates — SQL queries (AXL operations don't exist)
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def _extract_softkey_templates(self, result: ExtractionResult) -> list[dict[str, Any]]:
|
|
133
|
+
"""Extract softkey templates via SQL (AXL list/get don't exist in v15.0)."""
|
|
134
|
+
logger.info("[%s] Querying softkey templates via SQL...", self.name)
|
|
135
|
+
|
|
136
|
+
# Load softkey ID → name mapping
|
|
137
|
+
softkey_names = self._load_softkey_type_names()
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
rows = self.conn.execute_sql(
|
|
141
|
+
"SELECT name, description, softkeyclause, softkeysetclause, "
|
|
142
|
+
"iksoftkeytemplate_base FROM softkeytemplate ORDER BY name"
|
|
143
|
+
)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
logger.warning("[%s] Softkey template SQL query failed: %s", self.name, exc)
|
|
146
|
+
result.errors.append(f"Softkey template SQL failed: {exc}")
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
result.total += len(rows)
|
|
150
|
+
templates: list[dict[str, Any]] = []
|
|
151
|
+
|
|
152
|
+
for row in rows:
|
|
153
|
+
name = row.get("name")
|
|
154
|
+
if not name:
|
|
155
|
+
result.failed += 1
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
parsed = self._parse_softkey_template(row, softkey_names)
|
|
160
|
+
templates.append(parsed)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.warning("[%s] Failed to parse softkey template %s: %s", self.name, name, exc)
|
|
163
|
+
result.failed += 1
|
|
164
|
+
result.errors.append(f"Parse failed for softkey template {name}: {exc}")
|
|
165
|
+
|
|
166
|
+
return templates
|
|
167
|
+
|
|
168
|
+
def _load_softkey_type_names(self) -> dict[str, str]:
|
|
169
|
+
"""Load typesoftkey enum → name mapping from CUCM DB."""
|
|
170
|
+
if _SOFTKEY_ID_CACHE:
|
|
171
|
+
return _SOFTKEY_ID_CACHE
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
rows = self.conn.execute_sql(
|
|
175
|
+
"SELECT enum, name FROM typesoftkey ORDER BY enum"
|
|
176
|
+
)
|
|
177
|
+
for row in rows:
|
|
178
|
+
enum_val = str(row.get("enum", ""))
|
|
179
|
+
sk_name = row.get("name", "")
|
|
180
|
+
if enum_val and sk_name:
|
|
181
|
+
_SOFTKEY_ID_CACHE[enum_val] = sk_name
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
logger.warning("[%s] typesoftkey query failed: %s", self.name, exc)
|
|
184
|
+
|
|
185
|
+
return _SOFTKEY_ID_CACHE
|
|
186
|
+
|
|
187
|
+
def _parse_softkey_template(
|
|
188
|
+
self, row: dict[str, Any], softkey_names: dict[str, str]
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""Parse a softkeytemplate SQL row into a structured dict.
|
|
191
|
+
|
|
192
|
+
Decodes softkeyclause and softkeysetclause into human-readable form.
|
|
193
|
+
"""
|
|
194
|
+
name = row.get("name", "")
|
|
195
|
+
description = row.get("description", "")
|
|
196
|
+
base_pkid = row.get("iksoftkeytemplate_base", "")
|
|
197
|
+
|
|
198
|
+
# Decode softkeyclause: "0:1:2:3:4" → ["Undefined", "Redial", "NewCall", ...]
|
|
199
|
+
softkey_clause = row.get("softkeyclause", "")
|
|
200
|
+
available_softkeys: list[str] = []
|
|
201
|
+
if softkey_clause:
|
|
202
|
+
for sk_id in softkey_clause.split(":"):
|
|
203
|
+
sk_name = softkey_names.get(sk_id, f"unknown_{sk_id}")
|
|
204
|
+
available_softkeys.append(sk_name)
|
|
205
|
+
|
|
206
|
+
# Decode softkeysetclause: semicolon-separated per-state lists
|
|
207
|
+
# Each state: colon-separated softkey IDs
|
|
208
|
+
softkey_set_clause = row.get("softkeysetclause", "")
|
|
209
|
+
call_states: dict[str, list[str]] = {}
|
|
210
|
+
if softkey_set_clause:
|
|
211
|
+
state_parts = softkey_set_clause.split(";")
|
|
212
|
+
for i, state_data in enumerate(state_parts):
|
|
213
|
+
if i >= len(SOFTKEY_STATE_ORDER):
|
|
214
|
+
break
|
|
215
|
+
state_name = SOFTKEY_STATE_ORDER[i]
|
|
216
|
+
keys: list[str] = []
|
|
217
|
+
if state_data:
|
|
218
|
+
for sk_id in state_data.split(":"):
|
|
219
|
+
sk_name = softkey_names.get(sk_id, f"unknown_{sk_id}")
|
|
220
|
+
keys.append(sk_name)
|
|
221
|
+
call_states[state_name] = keys
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"name": name,
|
|
225
|
+
"description": description,
|
|
226
|
+
"base_template_pkid": base_pkid,
|
|
227
|
+
"available_softkeys": available_softkeys,
|
|
228
|
+
"call_states": call_states,
|
|
229
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tier 4 feature gap extractor — recording profiles, remote destinations,
|
|
2
|
+
transformation patterns, extension mobility device profiles.
|
|
3
|
+
|
|
4
|
+
Wave 1: extract + flag only. No mappers or execution handlers.
|
|
5
|
+
(from docs/prompts/tier4-feature-gap-extraction.md)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
14
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
RECORDING_PROFILE_TAGS = {"name": "", "recordingCssName": "", "recorderDestination": ""}
|
|
19
|
+
REMOTE_DEST_PROFILE_TAGS = {"name": "", "description": "", "userId": ""}
|
|
20
|
+
CALLING_PARTY_XFORM_TAGS = {
|
|
21
|
+
"pattern": "", "description": "", "callingSearchSpaceName": "",
|
|
22
|
+
"routePartitionName": "", "callingPartyTransformationMask": "",
|
|
23
|
+
"callingPartyPrefixDigits": "", "digitDiscardInstructionName": "",
|
|
24
|
+
}
|
|
25
|
+
CALLED_PARTY_XFORM_TAGS = {
|
|
26
|
+
"pattern": "", "description": "", "callingSearchSpaceName": "",
|
|
27
|
+
"routePartitionName": "", "calledPartyTransformationMask": "",
|
|
28
|
+
"calledPartyPrefixDigits": "", "digitDiscardInstructionName": "",
|
|
29
|
+
}
|
|
30
|
+
DEVICE_PROFILE_TAGS = {"name": "", "description": "", "product": "", "protocol": ""}
|
|
31
|
+
_BLOCKED_PARTITION_SQL = """\
|
|
32
|
+
SELECT n.dnorpattern, rp.name as partition_name, eu.userid
|
|
33
|
+
FROM numplan n
|
|
34
|
+
JOIN routepartition rp ON rp.pkid = n.fkroutepartition
|
|
35
|
+
LEFT JOIN devicenumplanmap dnpm ON dnpm.fknumplan = n.pkid
|
|
36
|
+
LEFT JOIN device d ON d.pkid = dnpm.fkdevice
|
|
37
|
+
LEFT JOIN enduser eu ON eu.pkid = d.fkenduser
|
|
38
|
+
WHERE LOWER(rp.name) LIKE '%intercept%'
|
|
39
|
+
OR LOWER(rp.name) LIKE '%block%'
|
|
40
|
+
OR LOWER(rp.name) LIKE '%out_of_service%'
|
|
41
|
+
OR LOWER(rp.name) LIKE '%oos%'
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_CFA_VOICEMAIL_SQL = """\
|
|
45
|
+
SELECT n.dnorpattern, rp.name as partition_name,
|
|
46
|
+
cfwd.cfadestination, eu.userid
|
|
47
|
+
FROM numplan n
|
|
48
|
+
LEFT JOIN routepartition rp ON rp.pkid = n.fkroutepartition
|
|
49
|
+
JOIN callforwarddynamic cfwd ON cfwd.fknumplan = n.pkid
|
|
50
|
+
LEFT JOIN devicenumplanmap dnpm ON dnpm.fknumplan = n.pkid
|
|
51
|
+
LEFT JOIN device d ON d.pkid = dnpm.fkdevice
|
|
52
|
+
LEFT JOIN enduser eu ON eu.pkid = d.fkenduser
|
|
53
|
+
WHERE cfwd.cfadestination IS NOT NULL
|
|
54
|
+
AND cfwd.cfadestination != ''
|
|
55
|
+
AND cfwd.cfavoicemailenabled = 't'
|
|
56
|
+
AND NOT EXISTS (
|
|
57
|
+
SELECT 1 FROM device d2
|
|
58
|
+
JOIN devicenumplanmap dnpm2 ON dnpm2.fkdevice = d2.pkid
|
|
59
|
+
WHERE dnpm2.fknumplan = n.pkid
|
|
60
|
+
AND d2.tkclass = 1
|
|
61
|
+
AND d2.tkstatus_registrationstate = 2
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Tier4Extractor(BaseExtractor):
|
|
67
|
+
"""Extract Tier 4 feature gap data from CUCM.
|
|
68
|
+
|
|
69
|
+
Extracts:
|
|
70
|
+
- Recording profiles (Item 1)
|
|
71
|
+
- Remote destination profiles / SNR (Item 2)
|
|
72
|
+
- Calling/Called party transformation patterns (Item 4)
|
|
73
|
+
- Extension Mobility device profiles (Item 6)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name = "tier4"
|
|
77
|
+
|
|
78
|
+
def __init__(self, connection: AXLConnection) -> None:
|
|
79
|
+
super().__init__(connection)
|
|
80
|
+
self.results: dict[str, list[dict[str, Any]]] = {}
|
|
81
|
+
|
|
82
|
+
def extract(self) -> ExtractionResult:
|
|
83
|
+
result = ExtractionResult(extractor=self.name)
|
|
84
|
+
total = 0
|
|
85
|
+
total += self._extract_type(
|
|
86
|
+
result, "recording_profiles",
|
|
87
|
+
"listRecordingProfile", {"name": "%"}, RECORDING_PROFILE_TAGS,
|
|
88
|
+
)
|
|
89
|
+
total += self._extract_type(
|
|
90
|
+
result, "remote_destination_profiles",
|
|
91
|
+
"listRemoteDestinationProfile", {"name": "%"}, REMOTE_DEST_PROFILE_TAGS,
|
|
92
|
+
)
|
|
93
|
+
total += self._extract_type(
|
|
94
|
+
result, "calling_party_transformations",
|
|
95
|
+
"listCallingPartyTransformationPattern", {"pattern": "%"},
|
|
96
|
+
CALLING_PARTY_XFORM_TAGS,
|
|
97
|
+
)
|
|
98
|
+
total += self._extract_type(
|
|
99
|
+
result, "called_party_transformations",
|
|
100
|
+
"listCalledPartyTransformationPattern", {"pattern": "%"},
|
|
101
|
+
CALLED_PARTY_XFORM_TAGS,
|
|
102
|
+
)
|
|
103
|
+
total += self._extract_type(
|
|
104
|
+
result, "device_profiles",
|
|
105
|
+
"listDeviceProfile", {"name": "%"}, DEVICE_PROFILE_TAGS,
|
|
106
|
+
)
|
|
107
|
+
total += self._extract_intercept_candidates(result)
|
|
108
|
+
result.total = total
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
def _extract_type(
|
|
112
|
+
self,
|
|
113
|
+
result: ExtractionResult,
|
|
114
|
+
key: str,
|
|
115
|
+
method: str,
|
|
116
|
+
search_criteria: dict[str, str],
|
|
117
|
+
returned_tags: dict[str, str],
|
|
118
|
+
) -> int:
|
|
119
|
+
try:
|
|
120
|
+
items = self.paginated_list(method, search_criteria, returned_tags)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
msg = f"{method} failed: {exc}"
|
|
123
|
+
logger.warning("[%s] %s", self.name, msg)
|
|
124
|
+
result.errors.append(msg)
|
|
125
|
+
self.results[key] = []
|
|
126
|
+
return 0
|
|
127
|
+
self.results[key] = items
|
|
128
|
+
logger.info("[%s] %s: %d objects", self.name, key, len(items))
|
|
129
|
+
return len(items)
|
|
130
|
+
|
|
131
|
+
def _extract_intercept_candidates(self, result: ExtractionResult) -> int:
|
|
132
|
+
"""Detect intercept-like configurations via SQL heuristics."""
|
|
133
|
+
seen: dict[str, dict[str, Any]] = {}
|
|
134
|
+
try:
|
|
135
|
+
rows = self.conn.execute_sql(_BLOCKED_PARTITION_SQL)
|
|
136
|
+
for row in rows:
|
|
137
|
+
dn = row.get("dnorpattern", "")
|
|
138
|
+
if not dn:
|
|
139
|
+
continue
|
|
140
|
+
seen[dn] = {
|
|
141
|
+
"userid": row.get("userid") or "",
|
|
142
|
+
"dn": dn,
|
|
143
|
+
"partition": row.get("partition_name") or "",
|
|
144
|
+
"signal_type": "blocked_partition",
|
|
145
|
+
"forward_destination": "",
|
|
146
|
+
"voicemail_enabled": False,
|
|
147
|
+
}
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
msg = f"Intercept blocked-partition SQL failed: {exc}"
|
|
150
|
+
logger.warning("[%s] %s", self.name, msg)
|
|
151
|
+
result.errors.append(msg)
|
|
152
|
+
try:
|
|
153
|
+
rows = self.conn.execute_sql(_CFA_VOICEMAIL_SQL)
|
|
154
|
+
for row in rows:
|
|
155
|
+
dn = row.get("dnorpattern", "")
|
|
156
|
+
if not dn or dn in seen:
|
|
157
|
+
continue
|
|
158
|
+
seen[dn] = {
|
|
159
|
+
"userid": row.get("userid") or "",
|
|
160
|
+
"dn": dn,
|
|
161
|
+
"partition": row.get("partition_name") or "",
|
|
162
|
+
"signal_type": "cfa_voicemail",
|
|
163
|
+
"forward_destination": row.get("cfadestination") or "",
|
|
164
|
+
"voicemail_enabled": True,
|
|
165
|
+
}
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
msg = f"Intercept CFA-voicemail SQL failed: {exc}"
|
|
168
|
+
logger.warning("[%s] %s", self.name, msg)
|
|
169
|
+
result.errors.append(msg)
|
|
170
|
+
candidates = list(seen.values())
|
|
171
|
+
self.results["intercept_candidates"] = candidates
|
|
172
|
+
logger.info("[%s] intercept_candidates: %d objects", self.name, len(candidates))
|
|
173
|
+
return len(candidates)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Users extractor — End Users.
|
|
2
|
+
|
|
3
|
+
Extracts CUCM End Users with associated device references, directory info,
|
|
4
|
+
and voicemail associations. Feeds user_mapper (03b §2), css_mapper
|
|
5
|
+
(cross-ref #18), and voicemail_mapper (cross-ref #26).
|
|
6
|
+
|
|
7
|
+
Sources:
|
|
8
|
+
- 02b-cucm-extraction.md §2.2 (users extractor spec)
|
|
9
|
+
- 03b-transform-mappers.md §2 (user_mapper field mapping)
|
|
10
|
+
- 02-normalization-architecture.md cross-ref #3, #4, #18, #26
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
21
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
22
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
# --- returnedTags constants (from 02b §2.2) ---
|
|
28
|
+
|
|
29
|
+
USER_LIST_RETURNED_TAGS = {
|
|
30
|
+
'firstName': '',
|
|
31
|
+
'lastName': '',
|
|
32
|
+
'userid': '',
|
|
33
|
+
'mailid': '',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
USER_GET_RETURNED_TAGS = {
|
|
37
|
+
'firstName': '', # Maps to Webex firstName
|
|
38
|
+
'lastName': '', # Maps to Webex lastName
|
|
39
|
+
'mailid': '', # Maps to Webex emails[] primary
|
|
40
|
+
'userid': '', # Fallback for emails[] if mailid empty
|
|
41
|
+
'telephoneNumber': '', # User's primary phone number
|
|
42
|
+
'department': '', # Maps to Webex department
|
|
43
|
+
'title': '', # Maps to Webex title
|
|
44
|
+
'manager': '', # Maps to Webex manager/managerId
|
|
45
|
+
'directoryUri': '', # Informational — "Webex auto-assigns SIP address"
|
|
46
|
+
'userLocale': '', # Informational — "Not a People API field"
|
|
47
|
+
'selfService': '', # Voicemail pilot — "Mapped by voicemail_mapper"
|
|
48
|
+
'associatedDevices': '', # Cross-ref #3: user_has_device
|
|
49
|
+
'enableCti': '', # CTI-enabled flag
|
|
50
|
+
'primaryExtension': '', # Cross-ref #4: user_has_primary_dn
|
|
51
|
+
'callingSearchSpaceName': '', # Cross-ref #18: user_has_css
|
|
52
|
+
'voiceMailProfile': '', # Cross-ref #26: user_has_voicemail_profile
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UserExtractor(BaseExtractor):
|
|
57
|
+
"""Extract CUCM End Users.
|
|
58
|
+
|
|
59
|
+
Two-step extraction:
|
|
60
|
+
1. listEndUser discovers all user PKIDs (summary fields only)
|
|
61
|
+
2. getEndUser per user retrieves the full field set including
|
|
62
|
+
associatedDevices, primaryExtension, CSS, and voicemail profile
|
|
63
|
+
|
|
64
|
+
Results stored in self.results['users'].
|
|
65
|
+
|
|
66
|
+
(from 02b §2.2)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name = "users"
|
|
70
|
+
|
|
71
|
+
def __init__(self, connection: AXLConnection) -> None:
|
|
72
|
+
super().__init__(connection)
|
|
73
|
+
self.results: dict[str, list[dict[str, Any]]] = {
|
|
74
|
+
'users': [],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def extract(self) -> ExtractionResult:
|
|
78
|
+
"""Discover users via AXL listEndUser, then get full detail.
|
|
79
|
+
|
|
80
|
+
Falls back to executeSQLQuery if listEndUser is blocked (EPR error).
|
|
81
|
+
This happens on cloud-connected CUCMs and some LDAP-synced deployments.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
return self._extract_via_axl()
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
exc_str = str(exc)
|
|
87
|
+
# EPR errors: cloud-connected CUCM or service not fully activated
|
|
88
|
+
# AttributeError: WSDL doesn't define EndUser operations (thin AXL)
|
|
89
|
+
# Fault: AXL service rejects the method
|
|
90
|
+
if any(s in exc_str for s in ("EPR", "Operation not found", "has no operation")):
|
|
91
|
+
logger.warning(
|
|
92
|
+
"listEndUser AXL unavailable (%s) — falling back to SQL",
|
|
93
|
+
exc,
|
|
94
|
+
)
|
|
95
|
+
return self._extract_via_sql()
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def _extract_via_axl(self) -> ExtractionResult:
|
|
99
|
+
"""Standard AXL extraction: listEndUser → getEndUser."""
|
|
100
|
+
errors: list[str] = []
|
|
101
|
+
failed = 0
|
|
102
|
+
|
|
103
|
+
summary_list = self.paginated_list(
|
|
104
|
+
"listEndUser",
|
|
105
|
+
search_criteria={"firstName": "%"},
|
|
106
|
+
returned_tags=USER_LIST_RETURNED_TAGS,
|
|
107
|
+
)
|
|
108
|
+
total = len(summary_list)
|
|
109
|
+
logger.info("Found %d end users via listEndUser", total)
|
|
110
|
+
|
|
111
|
+
for i, user_summary in enumerate(summary_list, 1):
|
|
112
|
+
if i % 200 == 0:
|
|
113
|
+
console.print(f" users: {i}/{total}...")
|
|
114
|
+
userid = user_summary.get("userid", "<unknown>")
|
|
115
|
+
try:
|
|
116
|
+
raw = self.get_detail("getEndUser", userid=userid)
|
|
117
|
+
if raw is None:
|
|
118
|
+
failed += 1
|
|
119
|
+
errors.append(f"getEndUser returned None for '{userid}'")
|
|
120
|
+
continue
|
|
121
|
+
raw["associatedDevices"] = to_list(
|
|
122
|
+
raw.get("associatedDevices"), "device"
|
|
123
|
+
)
|
|
124
|
+
self.results["users"].append(raw)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
failed += 1
|
|
127
|
+
errors.append(f"getEndUser failed for '{userid}': {exc}")
|
|
128
|
+
|
|
129
|
+
return ExtractionResult(
|
|
130
|
+
extractor=self.name, total=total, failed=failed, errors=errors,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _extract_via_sql(self) -> ExtractionResult:
|
|
134
|
+
"""SQL fallback when AXL EndUser operations are blocked.
|
|
135
|
+
|
|
136
|
+
Uses executeSQLQuery to read the enduser table directly. Joins to
|
|
137
|
+
enduserdevicemap + device for associatedDevices, and numplan for
|
|
138
|
+
primaryExtension. Produces dicts matching the AXL getEndUser shape
|
|
139
|
+
so downstream normalizers work identically.
|
|
140
|
+
|
|
141
|
+
Verified against live CUCM 15.0 (2026-03-23): executeSQLQuery works
|
|
142
|
+
even when listEndUser/getEndUser return EPR errors.
|
|
143
|
+
"""
|
|
144
|
+
errors: list[str] = []
|
|
145
|
+
|
|
146
|
+
# Step 1: Get all active end users (exclude system/token users)
|
|
147
|
+
user_rows = self.conn.execute_sql(
|
|
148
|
+
"SELECT eu.pkid, eu.userid, eu.firstname, eu.lastname, "
|
|
149
|
+
"eu.mailid, eu.telephonenumber, eu.department, eu.title, "
|
|
150
|
+
"eu.manager, eu.directoryuri, eu.allowcticontrolflag "
|
|
151
|
+
"FROM enduser eu "
|
|
152
|
+
"WHERE eu.status = 1 AND eu.userid NOT LIKE 'Token_User_%'"
|
|
153
|
+
)
|
|
154
|
+
logger.info("Found %d end users via SQL", len(user_rows))
|
|
155
|
+
|
|
156
|
+
# Step 2: Get device associations (user → phone name)
|
|
157
|
+
# Try enduserdevicemap first (explicit associations), then fall back
|
|
158
|
+
# to device.fkenduser (ownerUserName). Both represent user→device links
|
|
159
|
+
# but enduserdevicemap is only populated when devices are added to a
|
|
160
|
+
# user's "Controlled Devices" list via GUI/updateEndUser.
|
|
161
|
+
device_rows = self.conn.execute_sql(
|
|
162
|
+
"SELECT eu.userid, d.name AS device_name "
|
|
163
|
+
"FROM enduser eu "
|
|
164
|
+
"JOIN device d ON d.fkenduser = eu.pkid "
|
|
165
|
+
"WHERE eu.status = 1 "
|
|
166
|
+
"UNION "
|
|
167
|
+
"SELECT eu.userid, d.name AS device_name "
|
|
168
|
+
"FROM enduser eu "
|
|
169
|
+
"JOIN enduserdevicemap edm ON eu.pkid = edm.fkenduser "
|
|
170
|
+
"JOIN device d ON edm.fkdevice = d.pkid "
|
|
171
|
+
"WHERE eu.status = 1"
|
|
172
|
+
)
|
|
173
|
+
user_devices: dict[str, list[str]] = {}
|
|
174
|
+
for row in device_rows:
|
|
175
|
+
uid = row.get("userid", "")
|
|
176
|
+
dname = row.get("device_name", "")
|
|
177
|
+
if uid and dname:
|
|
178
|
+
user_devices.setdefault(uid, []).append(dname)
|
|
179
|
+
|
|
180
|
+
# Step 3: Get primary extensions (line 1 of first physical phone)
|
|
181
|
+
# Uses device.fkenduser (ownerUserName) — same as step 2.
|
|
182
|
+
# Prefers physical phones (SEP*) over softphones (CSF*) for primary DN.
|
|
183
|
+
primary_dn_rows = self.conn.execute_sql(
|
|
184
|
+
"SELECT eu.userid, np.dnorpattern AS pattern, rp.name AS partition "
|
|
185
|
+
"FROM enduser eu "
|
|
186
|
+
"JOIN device d ON d.fkenduser = eu.pkid "
|
|
187
|
+
"JOIN devicenumplanmap dnmap ON d.pkid = dnmap.fkdevice "
|
|
188
|
+
"JOIN numplan np ON dnmap.fknumplan = np.pkid "
|
|
189
|
+
"LEFT JOIN routepartition rp ON np.fkroutepartition = rp.pkid "
|
|
190
|
+
"WHERE eu.status = 1 AND dnmap.numplanindex = 1 "
|
|
191
|
+
"AND d.name NOT LIKE 'CSF%'"
|
|
192
|
+
)
|
|
193
|
+
user_primary_dn: dict[str, dict[str, str | None]] = {}
|
|
194
|
+
for row in primary_dn_rows:
|
|
195
|
+
uid = row.get("userid", "")
|
|
196
|
+
if uid and uid not in user_primary_dn:
|
|
197
|
+
user_primary_dn[uid] = {
|
|
198
|
+
"pattern": row.get("pattern"),
|
|
199
|
+
"routePartitionName": row.get("partition"),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Step 4: Build user dicts matching AXL getEndUser shape
|
|
203
|
+
for row in user_rows:
|
|
204
|
+
userid = row.get("userid", "")
|
|
205
|
+
user_dict: dict[str, Any] = {
|
|
206
|
+
"pkid": row.get("pkid"),
|
|
207
|
+
"firstName": row.get("firstname"),
|
|
208
|
+
"lastName": row.get("lastname"),
|
|
209
|
+
"mailid": row.get("mailid"),
|
|
210
|
+
"userid": userid,
|
|
211
|
+
"telephoneNumber": row.get("telephonenumber"),
|
|
212
|
+
"department": row.get("department"),
|
|
213
|
+
"title": row.get("title"),
|
|
214
|
+
"manager": row.get("manager"),
|
|
215
|
+
"directoryUri": row.get("directoryuri"),
|
|
216
|
+
"enableCti": row.get("allowcticontrolflag"),
|
|
217
|
+
"associatedDevices": user_devices.get(userid, []),
|
|
218
|
+
"primaryExtension": user_primary_dn.get(userid),
|
|
219
|
+
# These require AXL EndUser object — not available via SQL
|
|
220
|
+
"callingSearchSpaceName": None,
|
|
221
|
+
"voiceMailProfile": None,
|
|
222
|
+
"selfService": None,
|
|
223
|
+
"userLocale": None,
|
|
224
|
+
"_extracted_via": "sql",
|
|
225
|
+
}
|
|
226
|
+
self.results["users"].append(user_dict)
|
|
227
|
+
|
|
228
|
+
logger.info(
|
|
229
|
+
"End users via SQL: %d extracted, %d with devices, %d with primary DN",
|
|
230
|
+
len(user_rows),
|
|
231
|
+
sum(1 for u in self.results["users"] if u["associatedDevices"]),
|
|
232
|
+
sum(1 for u in self.results["users"] if u["primaryExtension"]),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return ExtractionResult(
|
|
236
|
+
extractor=self.name,
|
|
237
|
+
total=len(user_rows),
|
|
238
|
+
failed=0,
|
|
239
|
+
errors=errors,
|
|
240
|
+
)
|