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,74 @@
|
|
|
1
|
+
# cucm/ — CUCM Extraction Layer (Phase 03)
|
|
2
|
+
|
|
3
|
+
Connects to CUCM via AXL SOAP and extracts raw configuration into the SQLite store. Output is `DiscoveryResult.raw_data` — a dict of dicts of lists that feeds directly into `transform/pipeline.py:normalize_discovery()`.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `connection.py` | `AXLConnection` — zeep SOAP client with certificate handling and retry |
|
|
10
|
+
| `discovery.py` | `run_discovery(store, config)` — orchestrates all extractors in order, writes results to store |
|
|
11
|
+
| `unity_connection.py` | `UnityConnectionClient` — REST client for Unity Connection per-user VM settings |
|
|
12
|
+
| `extractors/base.py` | `BaseExtractor` ABC + `ExtractionResult` dataclass |
|
|
13
|
+
| `extractors/devices.py` | Phones (SEP) and their line appearances |
|
|
14
|
+
| `extractors/features.py` | Hunt pilots/lists/groups, call park, pickup groups, CTI route points, paging groups |
|
|
15
|
+
| `extractors/locations.py` | CUCM locations, device pools, datetime groups |
|
|
16
|
+
| `extractors/routing.py` | Gateways, SIP trunks, route groups, route lists, route patterns, CSS, partitions |
|
|
17
|
+
| `extractors/shared_lines.py` | Shared line detection (post-normalization, not an AXL extractor) |
|
|
18
|
+
| `extractors/templates.py` | Button templates + softkey templates (via SQL — AXL doesn't expose softkey template list) |
|
|
19
|
+
| `extractors/users.py` | EndUsers, lines (DNs), voicemail profiles |
|
|
20
|
+
| `extractors/voicemail.py` | Unity Connection voicemail pilot, voicemail profiles |
|
|
21
|
+
| `extractors/helpers.py` | `to_list()`, zeep response normalization helpers |
|
|
22
|
+
| `extractors/workspaces.py` | Common-area phone workspace classification (post-normalization) |
|
|
23
|
+
| `extractors/informational.py` | Tier 3: 20 informational object types (regions, SRST, MRG, app users, etc.) — report only |
|
|
24
|
+
| `extractors/tier4.py` | Tier 4 feature gaps: recording profiles, remote destinations, transformation patterns, EM device profiles |
|
|
25
|
+
| `extractors/remote_destinations.py` | Remote destinations for SNR |
|
|
26
|
+
| `extractors/e911.py` | ELIN groups, geo locations |
|
|
27
|
+
| `extractors/device_profiles.py` | Extension Mobility device profiles |
|
|
28
|
+
| `extractors/moh.py` | Music on Hold sources |
|
|
29
|
+
| `extractors/announcements.py` | Announcements / media resources |
|
|
30
|
+
|
|
31
|
+
## Extraction Order
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
locations → users → devices → features → routing → templates → voicemail → informational → tier4 → remote_destinations → e911 → device_profiles → moh → announcements
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Extractors are independent at extraction time — the order is a documentation convention, not a dependency constraint. All raw dicts go into `raw_data` keyed by extractor name.
|
|
38
|
+
|
|
39
|
+
## raw_data Structure
|
|
40
|
+
|
|
41
|
+
`raw_data` is a `dict[str, dict[str, list]]`:
|
|
42
|
+
- Top key = extractor group (`"devices"`, `"users"`, `"features"`, etc.)
|
|
43
|
+
- Inner key = object type (`"phones"`, `"users"`, `"button_templates"`, etc.)
|
|
44
|
+
- Value = list of raw CUCM dicts (as returned by zeep, pre-normalized by `to_list()`)
|
|
45
|
+
|
|
46
|
+
`normalize_discovery()` in `transform/pipeline.py` consumes `raw_data` via `RAW_DATA_MAPPING`.
|
|
47
|
+
|
|
48
|
+
**Executive/assistant data** is extracted via SQL (no AXL list/get operations exist):
|
|
49
|
+
- `raw_data["features"]["executive_assistant_pairs"]` — pair dicts from `executiveassistant` table join
|
|
50
|
+
- `raw_data["features"]["executive_settings"]` — user service subscription dicts from `endusersubscribedservice` join
|
|
51
|
+
|
|
52
|
+
**Intercept candidates** are extracted via SQL heuristics:
|
|
53
|
+
- `raw_data["tier4"]["intercept_candidates"]` — heuristically-detected intercept-like configurations (blocked partition DNs, CFA-to-voicemail with no registered device). Uses two SQL queries for partition name pattern matching and call forwarding state detection.
|
|
54
|
+
|
|
55
|
+
**Standalone extractors** (wired into discovery.py alongside the 9 original extractors):
|
|
56
|
+
- `raw_data["remote_destinations"]["remote_destinations"]` — SNR remote destinations
|
|
57
|
+
- `raw_data["e911"]["elin_groups"]` — ELIN groups for E911
|
|
58
|
+
- `raw_data["e911"]["geo_locations"]` — geo locations for E911
|
|
59
|
+
- `raw_data["device_profiles"]["device_profiles"]` — Extension Mobility device profiles
|
|
60
|
+
- `raw_data["moh"]["moh_sources"]` — Music on Hold audio sources
|
|
61
|
+
- `raw_data["announcements"]["announcements"]` — announcement / media resources
|
|
62
|
+
|
|
63
|
+
## Key Gotchas
|
|
64
|
+
|
|
65
|
+
- **Softkey templates require SQL.** `addSoftkeyTemplate`/`listSoftkeyTemplate` don't exist in AXL v15.0. The `TemplateExtractor` uses direct SQL (`executeSQLQuery`) to fetch softkey template data. See `extractors/templates.py`.
|
|
66
|
+
- **Button templates use `<buttonNumber>` not `<index>`** in `addPhoneButtonTemplate`. The extractor normalizes this on the way out.
|
|
67
|
+
- **BLF name mismatch.** The feature is called "Speed Dial BLF" in `addPhone` but appears as "Busy Lamp Field" in `getPhone` responses. Extractors handle both.
|
|
68
|
+
- **Raw phones must be preserved.** `normalize_phone()` creates a `CanonicalDevice` but loses the raw dict. `pipeline.py` re-stores each phone as `MigrationObject(canonical_id="phone:{name}", pre_migration_state=phone)` so mappers can read `store.get_objects("phone")` and access `speeddials`, `busyLampFields`, and per-line call forwarding.
|
|
69
|
+
- **Unity Connection is optional.** If no Unity Connection config is present, `unity_connection.py` is skipped and `raw_data["voicemail"]["unity_user_settings"]` is absent.
|
|
70
|
+
- **Base station MACs require Cisco Bifrost.** DECT base station MAC validation requires the Cisco manufacturing database — fake MACs fail provisioning. Not an extraction issue but affects test beds.
|
|
71
|
+
- **CTI Route Points use protocol "CTI Route Point"** in AXL — different from standard phone protocols. Use that exact string in `addCTIRoutePoint`.
|
|
72
|
+
- **Pickup group members via `updateLine`**, not `addCallPickupGroup`. Creating a pickup group with `<members>` fails on CUCM 15.0 (null priority FK constraint). The extractor reads the existing members; provisioning test bed uses `updateLine` pattern.
|
|
73
|
+
- **`getPhone` now extracts device-settings fields.** `commonPhoneConfigName`, `productSpecificConfiguration`, `userLocale`, `networkLocale`, `dndOption`, `dndStatus`, and `enableExtensionMobility` are all in `PHONE_GET_RETURNED_TAGS` for the DeviceSettingsMapper.
|
|
74
|
+
- **InformationalExtractor fetches CPC details.** After listing Common Phone Configs, the extractor calls `getCommonPhoneConfig` per item to retrieve `vendorConfig` (model-specific XML settings).
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""AXL SOAP client via zeep.
|
|
2
|
+
|
|
3
|
+
Provides the AXL connection, version detection, and pagination helper
|
|
4
|
+
that all extractors use.
|
|
5
|
+
|
|
6
|
+
Sources:
|
|
7
|
+
- cucm-wxc-migration.md lines 301-309 (WSDL, auth, pagination)
|
|
8
|
+
- 02b-cucm-extraction.md §1 (connection setup, pagination strategy)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import urllib3
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from requests import Session
|
|
19
|
+
from requests.auth import HTTPBasicAuth
|
|
20
|
+
from zeep import Client, Settings
|
|
21
|
+
from zeep.helpers import serialize_object
|
|
22
|
+
from zeep.transports import Transport
|
|
23
|
+
|
|
24
|
+
# CUCM uses self-signed certs — suppress the noise
|
|
25
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AXLConnectionError(Exception):
|
|
31
|
+
"""Raised when the AXL connection cannot be established."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AXLConnection:
|
|
35
|
+
"""AXL SOAP client using zeep.
|
|
36
|
+
|
|
37
|
+
(from 02b §1: connection setup)
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
host: CUCM hostname or IP address.
|
|
41
|
+
username: AXL admin username.
|
|
42
|
+
password: AXL admin password.
|
|
43
|
+
version: CUCM AXL schema version (default "14.0").
|
|
44
|
+
verify_ssl: Whether to verify SSL certificates (default False —
|
|
45
|
+
CUCM typically uses self-signed certs).
|
|
46
|
+
timeout: SOAP request timeout in seconds (default 30).
|
|
47
|
+
wsdl_path: Path to a local AXL WSDL file. If provided, zeep loads
|
|
48
|
+
the schema locally instead of fetching from the CUCM server.
|
|
49
|
+
Download from CUCM Admin > Application > Plugins > "Cisco AXL Toolkit".
|
|
50
|
+
port: CUCM AXL port (default 8443).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
host: str,
|
|
56
|
+
username: str,
|
|
57
|
+
password: str,
|
|
58
|
+
version: str = "14.0",
|
|
59
|
+
verify_ssl: bool = False,
|
|
60
|
+
timeout: int = 30,
|
|
61
|
+
wsdl_path: str | None = None,
|
|
62
|
+
port: int = 8443,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.host = host
|
|
65
|
+
self.port = port
|
|
66
|
+
self.version = version
|
|
67
|
+
|
|
68
|
+
session = Session()
|
|
69
|
+
session.auth = HTTPBasicAuth(username, password)
|
|
70
|
+
session.verify = verify_ssl
|
|
71
|
+
transport = Transport(session=session, timeout=timeout)
|
|
72
|
+
|
|
73
|
+
# Use local WSDL if provided, otherwise fetch from CUCM
|
|
74
|
+
if wsdl_path:
|
|
75
|
+
self.wsdl_url = wsdl_path
|
|
76
|
+
else:
|
|
77
|
+
self.wsdl_url = f"https://{host}:{self.port}/axl/AXLAPIService?wsdl"
|
|
78
|
+
|
|
79
|
+
# strict=False: CUCM responses may contain elements not in the WSDL
|
|
80
|
+
# (e.g. isAnonymous in getSipTrunk on CUCM 15.0). Without this,
|
|
81
|
+
# zeep raises XMLSyntaxError for any unexpected element.
|
|
82
|
+
settings = Settings(strict=False, xml_huge_tree=True)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self.client = Client(
|
|
86
|
+
self.wsdl_url, transport=transport, settings=settings,
|
|
87
|
+
)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
raise AXLConnectionError(
|
|
90
|
+
f"Failed to load WSDL from {self.wsdl_url}: {exc}"
|
|
91
|
+
) from exc
|
|
92
|
+
|
|
93
|
+
# When using a local WSDL, zeep needs the service address pointed at the real CUCM
|
|
94
|
+
service_url = f"https://{host}:{self.port}/axl/"
|
|
95
|
+
try:
|
|
96
|
+
self.service = self.client.create_service(
|
|
97
|
+
"{http://www.cisco.com/AXLAPIService/}AXLAPIBinding",
|
|
98
|
+
service_url,
|
|
99
|
+
)
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
raise AXLConnectionError(
|
|
102
|
+
f"Failed to initialize AXL service at {service_url}: {exc}"
|
|
103
|
+
) from exc
|
|
104
|
+
|
|
105
|
+
def get_version(self) -> str:
|
|
106
|
+
"""Detect CUCM version via getCCMVersion().
|
|
107
|
+
|
|
108
|
+
(from cucm-wxc-migration.md line 309)
|
|
109
|
+
"""
|
|
110
|
+
response = self._serialize(self.service.getCCMVersion())
|
|
111
|
+
version_info = response.get("return", {})
|
|
112
|
+
if isinstance(version_info, dict):
|
|
113
|
+
comp = version_info.get("componentVersion", {})
|
|
114
|
+
if isinstance(comp, dict):
|
|
115
|
+
return comp.get("version", self.version)
|
|
116
|
+
return str(comp) if comp else self.version
|
|
117
|
+
return self.version
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Pagination helper
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def paginated_list(
|
|
124
|
+
self,
|
|
125
|
+
method_name: str,
|
|
126
|
+
search_criteria: dict[str, str],
|
|
127
|
+
returned_tags: dict[str, str],
|
|
128
|
+
page_size: int = 200,
|
|
129
|
+
) -> list[dict[str, Any]]:
|
|
130
|
+
"""Generic paginated AXL list operation.
|
|
131
|
+
|
|
132
|
+
(from 02b §1: pagination strategy)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
method_name: AXL method (e.g. 'listEndUser').
|
|
136
|
+
search_criteria: Filter dict (e.g. {'firstName': '%'} for all).
|
|
137
|
+
returned_tags: Fields to return (AXL only returns requested fields).
|
|
138
|
+
page_size: Rows per page (default 200, AXL max for most methods).
|
|
139
|
+
"""
|
|
140
|
+
all_results: list[dict[str, Any]] = []
|
|
141
|
+
skip = 0
|
|
142
|
+
while True:
|
|
143
|
+
raw = getattr(self.service, method_name)(
|
|
144
|
+
searchCriteria=search_criteria,
|
|
145
|
+
returnedTags=returned_tags,
|
|
146
|
+
first=str(page_size),
|
|
147
|
+
skip=str(skip),
|
|
148
|
+
)
|
|
149
|
+
response = self._serialize(raw)
|
|
150
|
+
batch = self._extract_rows(response)
|
|
151
|
+
if not batch:
|
|
152
|
+
break
|
|
153
|
+
all_results.extend(batch)
|
|
154
|
+
if len(batch) < page_size:
|
|
155
|
+
break # Last page
|
|
156
|
+
skip += page_size
|
|
157
|
+
return all_results
|
|
158
|
+
|
|
159
|
+
def get_detail(
|
|
160
|
+
self, method_name: str, **kwargs: Any
|
|
161
|
+
) -> dict[str, Any] | None:
|
|
162
|
+
"""Single-object get operation (e.g. getPhone by name or UUID).
|
|
163
|
+
|
|
164
|
+
Returns the inner result dict, or None on error.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
raw = getattr(self.service, method_name)(**kwargs)
|
|
168
|
+
response = self._serialize(raw)
|
|
169
|
+
ret = response.get("return", {})
|
|
170
|
+
# get responses wrap the object under a single key
|
|
171
|
+
if isinstance(ret, dict):
|
|
172
|
+
for key, value in ret.items():
|
|
173
|
+
if key != "_value_1" and value is not None:
|
|
174
|
+
return self._serialize(value)
|
|
175
|
+
return ret
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
logger.warning("[%s] %s", method_name, exc)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def execute_sql(self, query: str) -> list[dict[str, Any]]:
|
|
181
|
+
"""Run an executeSQLQuery against CUCM's informix database.
|
|
182
|
+
|
|
183
|
+
(from 02b §5: performance considerations for large clusters)
|
|
184
|
+
|
|
185
|
+
Returns list of row dicts. Each row is a dict mapping column name → value.
|
|
186
|
+
"""
|
|
187
|
+
raw = self.service.executeSQLQuery(sql=query)
|
|
188
|
+
response = self._serialize(raw)
|
|
189
|
+
rows = self._extract_rows(response)
|
|
190
|
+
# SQL rows may come back as lxml Elements (when using WSDL + zeep).
|
|
191
|
+
# Convert each row to a plain dict.
|
|
192
|
+
result: list[dict[str, Any]] = []
|
|
193
|
+
for row in rows:
|
|
194
|
+
if isinstance(row, dict):
|
|
195
|
+
result.append(row)
|
|
196
|
+
elif isinstance(row, list):
|
|
197
|
+
# List of lxml Elements: each has .tag and .text
|
|
198
|
+
row_dict: dict[str, Any] = {}
|
|
199
|
+
for elem in row:
|
|
200
|
+
tag = getattr(elem, "tag", None)
|
|
201
|
+
if tag:
|
|
202
|
+
# Strip namespace if present
|
|
203
|
+
tag = tag.split("}")[-1] if "}" in tag else tag
|
|
204
|
+
text = getattr(elem, "text", None)
|
|
205
|
+
if tag:
|
|
206
|
+
row_dict[tag] = text
|
|
207
|
+
result.append(row_dict)
|
|
208
|
+
elif hasattr(row, "__dict__"):
|
|
209
|
+
result.append(dict(row.__dict__))
|
|
210
|
+
else:
|
|
211
|
+
result.append({"_raw": str(row)})
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
# ------------------------------------------------------------------
|
|
215
|
+
# Internal helpers
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _serialize(obj: Any) -> dict[str, Any]:
|
|
220
|
+
"""Convert zeep typed objects to plain Python dicts.
|
|
221
|
+
|
|
222
|
+
Zeep with a local WSDL returns typed objects (e.g. GetPhoneRes)
|
|
223
|
+
instead of plain dicts. ``serialize_object`` converts them to
|
|
224
|
+
OrderedDicts recursively. We convert to regular dicts for consistency.
|
|
225
|
+
"""
|
|
226
|
+
if obj is None:
|
|
227
|
+
return {}
|
|
228
|
+
if isinstance(obj, dict):
|
|
229
|
+
return obj
|
|
230
|
+
serialized = serialize_object(obj, dict)
|
|
231
|
+
if isinstance(serialized, dict):
|
|
232
|
+
return serialized
|
|
233
|
+
return {"_raw": serialized}
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _extract_rows(response: Any) -> list[dict[str, Any]]:
|
|
237
|
+
"""Extract the row list from an AXL list/SQL response.
|
|
238
|
+
|
|
239
|
+
AXL responses: {'return': {'{objectType}': [rows...]}}
|
|
240
|
+
Zeep converts this to a dict with one key matching the object type.
|
|
241
|
+
(from 02b §3: base extractor _extract_rows)
|
|
242
|
+
"""
|
|
243
|
+
if response is None:
|
|
244
|
+
return []
|
|
245
|
+
# Extract the 'return' value from the response.
|
|
246
|
+
# Zeep response objects support dict-like access.
|
|
247
|
+
ret: Any = None
|
|
248
|
+
if hasattr(response, "get"):
|
|
249
|
+
ret = response.get("return")
|
|
250
|
+
elif hasattr(response, "__getitem__"):
|
|
251
|
+
try:
|
|
252
|
+
ret = response["return"]
|
|
253
|
+
except (KeyError, TypeError):
|
|
254
|
+
return []
|
|
255
|
+
else:
|
|
256
|
+
return []
|
|
257
|
+
if not ret:
|
|
258
|
+
return []
|
|
259
|
+
if isinstance(ret, dict):
|
|
260
|
+
for key, value in ret.items():
|
|
261
|
+
if key != "_value_1" and value is not None:
|
|
262
|
+
if isinstance(value, list):
|
|
263
|
+
return value
|
|
264
|
+
return [value]
|
|
265
|
+
return []
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Discovery orchestrator — runs all extractors and writes results to store.
|
|
2
|
+
|
|
3
|
+
Entry point for the extraction phase. Connects to CUCM via AXL SOAP,
|
|
4
|
+
runs each extractor in sequence, and writes raw CUCM dicts into the SQLite
|
|
5
|
+
store for pass 1 normalization.
|
|
6
|
+
|
|
7
|
+
Sources:
|
|
8
|
+
- 02b-cucm-extraction.md §1 (orchestration, extractor order, summary)
|
|
9
|
+
- 02-normalization-architecture.md (two-pass ELT pattern)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
22
|
+
from wxcli.migration.cucm.extractors.base import ExtractionResult
|
|
23
|
+
from wxcli.migration.cucm.extractors.devices import DeviceExtractor
|
|
24
|
+
from wxcli.migration.cucm.extractors.features import FeatureExtractor
|
|
25
|
+
from wxcli.migration.cucm.extractors.locations import LocationExtractor
|
|
26
|
+
from wxcli.migration.cucm.extractors.routing import RoutingExtractor
|
|
27
|
+
from wxcli.migration.cucm.extractors.users import UserExtractor
|
|
28
|
+
from wxcli.migration.cucm.extractors.templates import TemplateExtractor
|
|
29
|
+
from wxcli.migration.cucm.extractors.voicemail import VoicemailExtractor
|
|
30
|
+
from wxcli.migration.cucm.extractors.informational import InformationalExtractor
|
|
31
|
+
from wxcli.migration.cucm.extractors.tier4 import Tier4Extractor
|
|
32
|
+
from wxcli.migration.cucm.extractors.remote_destinations import RemoteDestinationExtractor
|
|
33
|
+
from wxcli.migration.cucm.extractors.e911 import E911Extractor
|
|
34
|
+
from wxcli.migration.cucm.extractors.device_profiles import DeviceProfileExtractor
|
|
35
|
+
from wxcli.migration.cucm.extractors.moh import MOHExtractor
|
|
36
|
+
from wxcli.migration.cucm.extractors.announcements import AnnouncementExtractor
|
|
37
|
+
from wxcli.migration.cucm.unity_connection import UnityConnectionClient
|
|
38
|
+
from wxcli.migration.store import MigrationStore
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
console = Console()
|
|
42
|
+
|
|
43
|
+
# Extraction order (from 02b §1).
|
|
44
|
+
# Extractors are independent at the extraction phase — order is flexible.
|
|
45
|
+
# shared_lines and workspaces are post-processing steps, not AXL extractors,
|
|
46
|
+
# and run during normalization pass 2.
|
|
47
|
+
EXTRACTOR_ORDER = [
|
|
48
|
+
"locations", # Device pools, datetime groups, CUCM locations — no dependencies
|
|
49
|
+
"users", # EndUsers — no extraction-phase dependencies
|
|
50
|
+
"devices", # Phones — no extraction-phase dependencies
|
|
51
|
+
"routing", # Partitions, CSS, route patterns, gateways, trunks — no dependencies
|
|
52
|
+
"features", # Hunt pilots, CTI RPs, call park, pickup, schedules — no dependencies
|
|
53
|
+
"voicemail", # Voicemail profiles, pilot numbers — no dependencies
|
|
54
|
+
"templates", # Button templates, softkey templates — no dependencies
|
|
55
|
+
"informational", # Tier 3: report-only informational objects — no dependencies
|
|
56
|
+
"tier4", # Tier 4 feature gaps: recording, SNR, transformations, EM profiles
|
|
57
|
+
"remote_destinations", # SNR remote destinations
|
|
58
|
+
"e911", # ELIN groups, geo locations
|
|
59
|
+
"device_profiles", # EM device profiles
|
|
60
|
+
"moh", # Music on Hold sources
|
|
61
|
+
"announcements", # Announcements / media resources
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DiscoveryResult:
|
|
66
|
+
"""Aggregate result of a full discovery run.
|
|
67
|
+
|
|
68
|
+
(from 02b §1: discovery run summary)
|
|
69
|
+
|
|
70
|
+
**Data flow contract for Phase 04 normalizers:**
|
|
71
|
+
|
|
72
|
+
Discovery does NOT write raw data to the SQLite store. Raw CUCM dicts
|
|
73
|
+
live in ``raw_data`` (in-memory). Phase 04 normalizers consume
|
|
74
|
+
``raw_data``, transform to canonical models, and write to the store
|
|
75
|
+
via ``MigrationStore.upsert_object()``.
|
|
76
|
+
|
|
77
|
+
**raw_data structure per extractor:**
|
|
78
|
+
|
|
79
|
+
- ``raw_data["locations"]``: ``device_pools``, ``datetime_groups``, ``cucm_locations``
|
|
80
|
+
- ``raw_data["users"]``: ``users``
|
|
81
|
+
- ``raw_data["devices"]``: ``phones`` (lines pre-filtered: speed dials removed)
|
|
82
|
+
- ``raw_data["routing"]``: ``partitions``, ``css_list``, ``route_patterns``,
|
|
83
|
+
``gateways``, ``sip_trunks``, ``route_groups``, ``route_lists``,
|
|
84
|
+
``translation_patterns``
|
|
85
|
+
- ``raw_data["features"]``: ``hunt_pilots``, ``hunt_lists``, ``line_groups``,
|
|
86
|
+
``cti_route_points``, ``call_parks``, ``pickup_groups``, ``time_schedules``,
|
|
87
|
+
``time_periods``
|
|
88
|
+
- ``raw_data["voicemail"]``: ``voicemail_profiles``, ``voicemail_pilots``,
|
|
89
|
+
and optionally ``unity_user_settings`` (dict keyed by CUCM userid)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self) -> None:
|
|
93
|
+
self.run_id: str = ""
|
|
94
|
+
self.cucm_version: str = ""
|
|
95
|
+
self.started_at: str = ""
|
|
96
|
+
self.completed_at: str = ""
|
|
97
|
+
self.extractor_results: dict[str, ExtractionResult] = {}
|
|
98
|
+
self.raw_data: dict[str, dict[str, Any]] = {}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def total_objects(self) -> int:
|
|
102
|
+
return sum(r.total for r in self.extractor_results.values())
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def total_failed(self) -> int:
|
|
106
|
+
return sum(r.failed for r in self.extractor_results.values())
|
|
107
|
+
|
|
108
|
+
def to_summary(self) -> dict[str, Any]:
|
|
109
|
+
"""Produce the journal summary dict (from 02b §1)."""
|
|
110
|
+
return {
|
|
111
|
+
"run_type": "discovery",
|
|
112
|
+
"run_id": self.run_id,
|
|
113
|
+
"started_at": self.started_at,
|
|
114
|
+
"completed_at": self.completed_at,
|
|
115
|
+
"cucm_version": self.cucm_version,
|
|
116
|
+
"extractors": {
|
|
117
|
+
name: {"total": r.total, "failed": r.failed}
|
|
118
|
+
for name, r in self.extractor_results.items()
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_discovery(
|
|
124
|
+
connection: AXLConnection,
|
|
125
|
+
store: MigrationStore,
|
|
126
|
+
unity_client: UnityConnectionClient | None = None,
|
|
127
|
+
) -> DiscoveryResult:
|
|
128
|
+
"""Run full CUCM discovery — extract all object types.
|
|
129
|
+
|
|
130
|
+
(from 02b §1: discovery orchestration)
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
connection: Established AXL SOAP connection.
|
|
134
|
+
store: Migration store for writing results and journal entries.
|
|
135
|
+
unity_client: Optional Unity Connection client for voicemail detail.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
DiscoveryResult with per-extractor counts, errors, and raw data.
|
|
139
|
+
"""
|
|
140
|
+
result = DiscoveryResult()
|
|
141
|
+
result.run_id = store.current_run_id
|
|
142
|
+
result.started_at = datetime.now(timezone.utc).isoformat()
|
|
143
|
+
|
|
144
|
+
# Detect CUCM version (from cucm-wxc-migration.md line 309)
|
|
145
|
+
try:
|
|
146
|
+
result.cucm_version = connection.get_version()
|
|
147
|
+
logger.info("CUCM version: %s", result.cucm_version)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.warning("Could not detect CUCM version: %s", exc)
|
|
150
|
+
result.cucm_version = "unknown"
|
|
151
|
+
|
|
152
|
+
# Build extractors in order (from 02b §1: EXTRACTOR_ORDER)
|
|
153
|
+
extractors: dict[str, Any] = {
|
|
154
|
+
"locations": LocationExtractor(connection),
|
|
155
|
+
"users": UserExtractor(connection),
|
|
156
|
+
"devices": DeviceExtractor(connection),
|
|
157
|
+
"routing": RoutingExtractor(connection),
|
|
158
|
+
"features": FeatureExtractor(connection),
|
|
159
|
+
"voicemail": VoicemailExtractor(connection, unity_client=unity_client),
|
|
160
|
+
"templates": TemplateExtractor(connection),
|
|
161
|
+
"informational": InformationalExtractor(connection),
|
|
162
|
+
"tier4": Tier4Extractor(connection),
|
|
163
|
+
"remote_destinations": RemoteDestinationExtractor(connection),
|
|
164
|
+
"e911": E911Extractor(connection),
|
|
165
|
+
"device_profiles": DeviceProfileExtractor(connection),
|
|
166
|
+
"moh": MOHExtractor(connection),
|
|
167
|
+
"announcements": AnnouncementExtractor(connection),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Run each extractor in order
|
|
171
|
+
for name in EXTRACTOR_ORDER:
|
|
172
|
+
extractor = extractors[name]
|
|
173
|
+
logger.info("--- Running extractor: %s ---", name)
|
|
174
|
+
console.print(f" Extracting {name}...")
|
|
175
|
+
try:
|
|
176
|
+
ext_result = extractor.extract()
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
logger.error("[%s] Extraction failed: %s", name, exc)
|
|
179
|
+
ext_result = ExtractionResult(extractor=name)
|
|
180
|
+
ext_result.errors.append(f"Unrecoverable error: {exc}")
|
|
181
|
+
|
|
182
|
+
result.extractor_results[name] = ext_result
|
|
183
|
+
result.raw_data[name] = extractor.results
|
|
184
|
+
console.print(f" {name}: {ext_result.total} objects")
|
|
185
|
+
if ext_result.errors:
|
|
186
|
+
for err in ext_result.errors:
|
|
187
|
+
console.print(f" [yellow]{name}: {err}[/yellow]")
|
|
188
|
+
|
|
189
|
+
# Extract Unity Connection per-user voicemail settings if client available
|
|
190
|
+
if unity_client is not None:
|
|
191
|
+
user_extractor = extractors["users"]
|
|
192
|
+
user_dicts = user_extractor.results.get("users", [])
|
|
193
|
+
user_ids = [u.get("userid", "") for u in user_dicts if u.get("userid")]
|
|
194
|
+
vm_extractor = extractors["voicemail"]
|
|
195
|
+
unity_settings = vm_extractor.extract_unity_settings(user_ids)
|
|
196
|
+
# Store Unity settings alongside AXL voicemail data
|
|
197
|
+
result.raw_data.setdefault("voicemail", {})["unity_user_settings"] = unity_settings
|
|
198
|
+
|
|
199
|
+
result.completed_at = datetime.now(timezone.utc).isoformat()
|
|
200
|
+
|
|
201
|
+
# Write summary to journal (from 02b §1: discovery run summary)
|
|
202
|
+
# Ensure the system sentinel object exists so the FK constraint is satisfied
|
|
203
|
+
store.conn.execute(
|
|
204
|
+
"""INSERT OR IGNORE INTO objects
|
|
205
|
+
(canonical_id, object_type, status, data, created_at, updated_at)
|
|
206
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
207
|
+
("system:discovery", "system", "discovered", "{}", result.completed_at, result.completed_at),
|
|
208
|
+
)
|
|
209
|
+
store.conn.commit()
|
|
210
|
+
summary = result.to_summary()
|
|
211
|
+
store.add_journal_entry(
|
|
212
|
+
entry_type="discovery_complete",
|
|
213
|
+
canonical_id="system:discovery",
|
|
214
|
+
resource_type="discovery",
|
|
215
|
+
request=summary,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
logger.info(
|
|
219
|
+
"Discovery complete: %d objects extracted (%d failed) across %d extractors",
|
|
220
|
+
result.total_objects,
|
|
221
|
+
result.total_failed,
|
|
222
|
+
len(result.extractor_results),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CUCM AXL extractors — one module per object domain."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Announcement extractor for CUCM AXL.
|
|
2
|
+
|
|
3
|
+
Pulls Announcements from CUCM AXL for custom audio migration advisory.
|
|
4
|
+
|
|
5
|
+
Sources:
|
|
6
|
+
- tier2-enterprise-expansion.md §2.4 (announcement extraction)
|
|
7
|
+
- AXL WSDL: listAnnouncement / getAnnouncement
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# returnedTags for listAnnouncement
|
|
20
|
+
ANNOUNCEMENT_LIST_RETURNED_TAGS = {
|
|
21
|
+
"name": "",
|
|
22
|
+
"description": "",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Additional fields from getAnnouncement
|
|
26
|
+
ANNOUNCEMENT_GET_RETURNED_TAGS = {
|
|
27
|
+
"name": "",
|
|
28
|
+
"description": "",
|
|
29
|
+
"announcementFile": "",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AnnouncementExtractor(BaseExtractor):
|
|
34
|
+
"""Extract Announcements from CUCM AXL.
|
|
35
|
+
|
|
36
|
+
Announcements are custom audio files uploaded to CUCM and referenced
|
|
37
|
+
by Auto Attendants, Call Queues, MOH sources, and other features.
|
|
38
|
+
|
|
39
|
+
(from tier2-enterprise-expansion.md §2.4)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = "announcements"
|
|
43
|
+
|
|
44
|
+
def __init__(self, connection) -> None:
|
|
45
|
+
super().__init__(connection)
|
|
46
|
+
self.results: dict[str, list[dict[str, Any]]] = {}
|
|
47
|
+
|
|
48
|
+
def extract(self) -> ExtractionResult:
|
|
49
|
+
result = ExtractionResult(extractor=self.name)
|
|
50
|
+
self.results["announcements"] = self._extract_announcements(result)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def _extract_announcements(
|
|
54
|
+
self, result: ExtractionResult
|
|
55
|
+
) -> list[dict[str, Any]]:
|
|
56
|
+
"""List all announcements, then get full detail for each."""
|
|
57
|
+
announcements: list[dict[str, Any]] = []
|
|
58
|
+
try:
|
|
59
|
+
summaries = self.paginated_list(
|
|
60
|
+
"listAnnouncement",
|
|
61
|
+
{"name": "%"},
|
|
62
|
+
ANNOUNCEMENT_LIST_RETURNED_TAGS,
|
|
63
|
+
)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
msg = f"listAnnouncement failed: {exc}"
|
|
66
|
+
logger.error("[%s] %s", self.name, msg)
|
|
67
|
+
result.errors.append(msg)
|
|
68
|
+
return announcements
|
|
69
|
+
|
|
70
|
+
for summary in summaries:
|
|
71
|
+
result.total += 1
|
|
72
|
+
ann_name = summary.get("name", "")
|
|
73
|
+
if not ann_name:
|
|
74
|
+
result.failed += 1
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
detail = self.get_detail("getAnnouncement", name=ann_name)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"[%s] getAnnouncement error for %s: %s",
|
|
81
|
+
self.name, ann_name, exc,
|
|
82
|
+
)
|
|
83
|
+
announcements.append(summary)
|
|
84
|
+
continue
|
|
85
|
+
if detail is None:
|
|
86
|
+
announcements.append(summary)
|
|
87
|
+
continue
|
|
88
|
+
# Merge list-level fields into detail
|
|
89
|
+
merged = {**summary, **detail}
|
|
90
|
+
announcements.append(merged)
|
|
91
|
+
|
|
92
|
+
return announcements
|