├── .github └── workflows │ └── github-ci.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── TEMPLATE └── email-template.html ├── cucm-exporter.py ├── cucm-exporter.spec ├── cucm.py ├── email_util.py ├── img ├── config_icon.png ├── config_icon2.png ├── config_icon3.png ├── cucm-exporter-screenshot1.png └── program_icon.ico ├── mail └── email-template.html └── requirements.txt /.github/workflows/github-ci.yaml: -------------------------------------------------------------------------------- 1 | name: cucm-exporter ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.platform }} 8 | strategy: 9 | matrix: 10 | python-version: [3.8] 11 | platform: [macos-latest, windows-latest] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | pip install pyinstaller 23 | - name: Lint with flake8 24 | run: | 25 | pip install flake8 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test commands 31 | run: | 32 | python cucm-exporter.py --help 33 | python cucm-exporter.py -a ${{ secrets.CUCM_SERVER }} --export users -o export_file.csv -u ${{ secrets.CUCM_USER }} -p ${{ secrets.CUCM_PASSWORD }} --version ${{ secrets.CUCM_VERSION }} 34 | python cucm-exporter.py -a ${{ secrets.CUCM_SERVER }} --export phones -o export_file.csv -u ${{ secrets.CUCM_USER }} -p ${{ secrets.CUCM_PASSWORD }} --version ${{ secrets.CUCM_VERSION }} 35 | python cucm-exporter.py -a ${{ secrets.CUCM_SERVER }} --export translations -o export_file.csv -u ${{ secrets.CUCM_USER }} -p ${{ secrets.CUCM_PASSWORD }} --version ${{ secrets.CUCM_VERSION }} 36 | python cucm-exporter.py -a ${{ secrets.CUCM_SERVER }} --export sip-trunks -o export_file.csv -u ${{ secrets.CUCM_USER }} -p ${{ secrets.CUCM_PASSWORD }} --version ${{ secrets.CUCM_VERSION }} 37 | - name: Build Binary executables 38 | run: python -m PyInstaller -F cucm-exporter.spec 39 | - uses: actions/upload-artifact@v1 40 | with: 41 | name: cucm-exporter-executable 42 | path: dist 43 | 44 | bump_version: 45 | needs: build 46 | runs-on: ${{ matrix.platform }} 47 | strategy: 48 | matrix: 49 | platform: [ubuntu-latest] 50 | steps: 51 | - uses: actions/checkout@master 52 | with: 53 | fetch-depth: "0" 54 | - name: Github Tag Bump 55 | uses: anothrNick/github-tag-action@1.19.0 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | WITH_V: false 59 | DEFAULT_BUMP: patch 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,macos,python,pycharm,windows,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=linux,macos,python,pycharm,windows,visualstudiocode 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### PyCharm ### 49 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 50 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 51 | 52 | # User-specific stuff 53 | .idea/**/workspace.xml 54 | .idea/**/tasks.xml 55 | .idea/**/usage.statistics.xml 56 | .idea/**/dictionaries 57 | .idea/**/shelf 58 | 59 | # Generated files 60 | .idea/**/contentModel.xml 61 | 62 | # Sensitive or high-churn files 63 | .idea/**/dataSources/ 64 | .idea/**/dataSources.ids 65 | .idea/**/dataSources.local.xml 66 | .idea/**/sqlDataSources.xml 67 | .idea/**/dynamic.xml 68 | .idea/**/uiDesigner.xml 69 | .idea/**/dbnavigator.xml 70 | 71 | # Gradle 72 | .idea/**/gradle.xml 73 | .idea/**/libraries 74 | 75 | # Gradle and Maven with auto-import 76 | # When using Gradle or Maven with auto-import, you should exclude module files, 77 | # since they will be recreated, and may cause churn. Uncomment if using 78 | # auto-import. 79 | # .idea/modules.xml 80 | # .idea/*.iml 81 | # .idea/modules 82 | # *.iml 83 | # *.ipr 84 | 85 | # CMake 86 | cmake-build-*/ 87 | 88 | # Mongo Explorer plugin 89 | .idea/**/mongoSettings.xml 90 | 91 | # File-based project format 92 | *.iws 93 | 94 | # IntelliJ 95 | out/ 96 | 97 | # mpeltonen/sbt-idea plugin 98 | .idea_modules/ 99 | 100 | # JIRA plugin 101 | atlassian-ide-plugin.xml 102 | 103 | # Cursive Clojure plugin 104 | .idea/replstate.xml 105 | 106 | # Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # Editor-based Rest Client 113 | .idea/httpRequests 114 | 115 | # Android studio 3.1+ serialized cache file 116 | .idea/caches/build_file_checksums.ser 117 | 118 | ### PyCharm Patch ### 119 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 120 | 121 | # *.iml 122 | # modules.xml 123 | # .idea/misc.xml 124 | # *.ipr 125 | 126 | # Sonarlint plugin 127 | .idea/**/sonarlint/ 128 | 129 | # SonarQube Plugin 130 | .idea/**/sonarIssues.xml 131 | 132 | # Markdown Navigator plugin 133 | .idea/**/markdown-navigator.xml 134 | .idea/**/markdown-navigator/ 135 | 136 | ### Python ### 137 | # Byte-compiled / optimized / DLL files 138 | __pycache__/ 139 | *.py[cod] 140 | *$py.class 141 | 142 | # C extensions 143 | *.so 144 | 145 | # Distribution / packaging 146 | .Python 147 | build/ 148 | develop-eggs/ 149 | dist/ 150 | downloads/ 151 | eggs/ 152 | .eggs/ 153 | lib/ 154 | lib64/ 155 | parts/ 156 | sdist/ 157 | var/ 158 | wheels/ 159 | pip-wheel-metadata/ 160 | share/python-wheels/ 161 | *.egg-info/ 162 | .installed.cfg 163 | *.egg 164 | MANIFEST 165 | 166 | # PyInstaller 167 | # Usually these files are written by a python script from a template 168 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 169 | *.manifest 170 | # *.spec 171 | 172 | # Installer logs 173 | pip-log.txt 174 | pip-delete-this-directory.txt 175 | 176 | # Unit test / coverage reports 177 | htmlcov/ 178 | .tox/ 179 | .nox/ 180 | .coverage 181 | .coverage.* 182 | .cache 183 | nosetests.xml 184 | coverage.xml 185 | *.cover 186 | .hypothesis/ 187 | .pytest_cache/ 188 | 189 | # Translations 190 | *.mo 191 | *.pot 192 | 193 | # Scrapy stuff: 194 | .scrapy 195 | 196 | # Sphinx documentation 197 | docs/_build/ 198 | 199 | # PyBuilder 200 | target/ 201 | 202 | # pyenv 203 | .python-version 204 | 205 | # pipenv 206 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 207 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 208 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 209 | # install all needed dependencies. 210 | #Pipfile.lock 211 | 212 | # celery beat schedule file 213 | celerybeat-schedule 214 | 215 | # SageMath parsed files 216 | *.sage.py 217 | 218 | # Spyder project settings 219 | .spyderproject 220 | .spyproject 221 | 222 | # Rope project settings 223 | .ropeproject 224 | 225 | # Mr Developer 226 | .mr.developer.cfg 227 | .project 228 | .pydevproject 229 | 230 | # mkdocs documentation 231 | /site 232 | 233 | # mypy 234 | .mypy_cache/ 235 | .dmypy.json 236 | dmypy.json 237 | 238 | # Pyre type checker 239 | .pyre/ 240 | 241 | ### VisualStudioCode ### 242 | .vscode/* 243 | !.vscode/settings.json 244 | !.vscode/tasks.json 245 | !.vscode/launch.json 246 | !.vscode/extensions.json 247 | 248 | ### VisualStudioCode Patch ### 249 | # Ignore all local history of files 250 | .history 251 | 252 | ### Windows ### 253 | # Windows thumbnail cache files 254 | Thumbs.db 255 | Thumbs.db:encryptable 256 | ehthumbs.db 257 | ehthumbs_vista.db 258 | 259 | # Dump file 260 | *.stackdump 261 | 262 | # Folder config file 263 | [Dd]esktop.ini 264 | 265 | # Recycle Bin used on file shares 266 | $RECYCLE.BIN/ 267 | 268 | # Windows Installer files 269 | *.cab 270 | *.msi 271 | *.msix 272 | *.msm 273 | *.msp 274 | 275 | # Windows shortcuts 276 | *.lnk 277 | 278 | # project specific ignore files 279 | /notes.txt 280 | /*.csv 281 | /.vscode 282 | /.venv -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at bradh11@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bradford haas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CUCM Exporter utility 2 | 3 | This tool was created in an effort to make exporting information from Cisco Unified Communications Manager (CUCM) easy. Some example use cases might include regularly exporting a user and phone number list to csv on a recurring schedule. 4 | 5 | - download [latest release](https://github.com/bradh11/cucm-exporter/releases/latest) 6 | 7 | ## Usage 8 | 9 | Now supporting full GUI via the amazing gooey python library. simply run the cucm-exporter without any following cli arguments. You can now run as a GUI or a CLI! 10 | 11 |  12 | 13 | This tool will be packaged as a standalone executable file that can be used with syntax as seen below: 14 | 15 | ``` 16 | cucm-exporter --help status: starting 2020-03-23 20:07:33.256787 17 | usage: cucm-exporter.exe [-h] --address CUCM_ADDRESS [--version {8.0,10.0,10.5,11.0,11.5,12.0,12.5}] --username 18 | CUCM_USERNAME --password CUCM_PASSWORD [--out FILENAME] [--timestamp] 19 | [--export {users,phones}] [--smtpserver SMTPSERVER] [--mailto MAILTO] 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | 24 | cucm connection: 25 | --address CUCM_ADDRESS, -a CUCM_ADDRESS 26 | specify cucm address 27 | --version {8.0,10.0,10.5,11.0,11.5,12.0,12.5}, -v {8.0,10.0,10.5,11.0,11.5,12.0,12.5} 28 | specify cucm AXL version 29 | --username CUCM_USERNAME, -u CUCM_USERNAME 30 | specify ucm account username with AXL permissions 31 | --password CUCM_PASSWORD, -p CUCM_PASSWORD 32 | specify ucm account password 33 | --export {users,phones}, -e {users,phones} 34 | specify what you want to export 35 | 36 | output file: 37 | --out FILENAME, -o FILENAME 38 | filename of export file (.csv format) - default="export.csv" 39 | --timestamp, -t append filename with timestamp 40 | 41 | email options: 42 | --smtpserver SMTPSERVER, -s SMTPSERVER 43 | smtp server name or ip address 44 | --mailto MAILTO, -m MAILTO 45 | send output to mail recipient 46 | 47 | ``` 48 | 49 | EXAMPLE 1 - running the executable 50 | 51 | ``` 52 | cucm-exporter -a 10.129.225.201 -v 11.0 -o "my file.csv" -u axlusername -p axlpassword -t --export users 53 | ``` 54 | 55 | EXAMPLE 2 - the raw python code can be run from source after installing dependencies `pip install -r requirements.txt` 56 | 57 | ``` 58 | python cucm-exporter.py -a 10.129.225.201 -v 11.0 -o "my file.csv" -u axlusername -p axlpassword -t --export users 59 | ``` 60 | -------------------------------------------------------------------------------- /TEMPLATE/email-template.html: -------------------------------------------------------------------------------- 1 |
Ready for your review...
3 | 4 |Regards,
5 |The CUCM Admin team
6 | -------------------------------------------------------------------------------- /cucm-exporter.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import json 3 | import sys 4 | import os 5 | from pathlib import Path 6 | from datetime import datetime 7 | from ciscoaxl import axl 8 | from ciscoris import ris 9 | import argparse 10 | from email_util import send_email 11 | from gooey import Gooey, GooeyParser 12 | import cucm 13 | 14 | BASE_DIR = Path(__file__).resolve().parent 15 | IMG_DIR = BASE_DIR.joinpath("img") 16 | 17 | start_time = datetime.now() 18 | print(f"status: starting {start_time}") 19 | 20 | 21 | class Unbuffered(object): 22 | # GOOEY config -> ensure unbuffered output mode 23 | def __init__(self, stream): 24 | self.stream = stream 25 | 26 | def write(self, data): 27 | self.stream.write(data) 28 | self.stream.flush() 29 | 30 | def writelines(self, datas): 31 | self.stream.writelines(datas) 32 | self.stream.flush() 33 | 34 | def __getattr__(self, attr): 35 | return getattr(self.stream, attr) 36 | 37 | 38 | sys.stdout = Unbuffered(sys.stdout) 39 | 40 | # GOOEY config -> GUI if no cli args, otherwise default to cli 41 | if len(sys.argv) >= 2: 42 | if not "--ignore-gooey" in sys.argv: 43 | sys.argv.append("--ignore-gooey") 44 | 45 | 46 | def get_fieldnames(content): 47 | """ 48 | Return the longest Dict Item for csv header writing 49 | """ 50 | item_length = 0 51 | csv_header = [] 52 | for item in content: 53 | if len(item) >= item_length: 54 | longest_item = item 55 | item_length = len(item) 56 | for key in longest_item.keys(): 57 | if key not in csv_header: 58 | csv_header.append(key) 59 | return csv_header 60 | 61 | 62 | def output_filename(filename, cli_args): 63 | """ 64 | Construct the output filename 65 | """ 66 | if cli_args.timestamp: 67 | date_time = datetime.now().strftime("%m-%d-%Y_%H.%M.%S") 68 | lname = filename.split(".")[0] 69 | rname = filename.split(".")[-1] 70 | new_filename = f"{lname}_{date_time}.{rname}" 71 | else: 72 | new_filename = filename 73 | 74 | return new_filename 75 | 76 | 77 | def write_csv(filename, cli_args, content): 78 | """ 79 | write output to csv file 80 | """ 81 | filename = output_filename(filename, cli_args) 82 | with open(filename, "w", newline="", encoding="utf-8") as csvfile: 83 | fieldnames = get_fieldnames(content) 84 | 85 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 86 | writer.writeheader() 87 | for each in content: 88 | writer.writerow(each) 89 | return filename 90 | 91 | 92 | @Gooey( 93 | program_name="CUCM Extraction Tool", 94 | program_description="Cisco Unified Communications Manager Tool", 95 | # default_size=(610, 850), 96 | menu=[ 97 | {"name": "File", "items": []}, 98 | {"name": "Tools", "items": []}, 99 | { 100 | "name": "Help", 101 | "items": [ 102 | { 103 | "type": "Link", 104 | "menuTitle": "Find us on Github", 105 | "url": "https://github.com/bradh11/cucm-exporter", 106 | } 107 | ], 108 | }, 109 | ], 110 | # image_dir=IMG_DIR, 111 | tabbed_groups=True, 112 | ) 113 | def main(): 114 | date_time = datetime.now().strftime("%m-%d-%Y_%H.%M.%S") 115 | 116 | # initialize the CLI parser 117 | parser = GooeyParser( 118 | description="Cisco Unified Communications Manager Tool") 119 | cucm_group = parser.add_argument_group(title="cucm connection") 120 | file_group = parser.add_argument_group(title="output file") 121 | email_group = parser.add_argument_group( 122 | title="optional email parameters", 123 | description="send the output to an email address", 124 | ) 125 | 126 | cucm_group.add_argument( 127 | "--address", 128 | "-a", 129 | action="store", 130 | dest="cucm_address", 131 | help="specify cucm address", 132 | default="ucm1.presidio.cloud", 133 | required=True, 134 | ) 135 | 136 | cucm_group.add_argument( 137 | "--version", 138 | "-v", 139 | action="store", 140 | dest="cucm_version", 141 | choices=["8.5", "10.0", "10.5", "11.0", "11.5", "12.0", "12.5"], 142 | help="specify cucm AXL version", 143 | required=False, 144 | default="11.0", 145 | ) 146 | 147 | cucm_group.add_argument( 148 | "--username", 149 | "-u", 150 | action="store", 151 | dest="cucm_username", 152 | help="specify ucm account username with AXL permissions", 153 | required=True, 154 | default="Administrator", 155 | ) 156 | 157 | cucm_group.add_argument( 158 | "--password", 159 | "-p", 160 | action="store", 161 | dest="cucm_password", 162 | help="specify ucm account password", 163 | required=True, 164 | default="Dev@1998", 165 | widget="PasswordField", 166 | ) 167 | 168 | file_group.add_argument( 169 | "--out", 170 | "-o", 171 | action="store", 172 | dest="filename", 173 | help='filename of export file (.csv format) - default="export.csv"', 174 | required=False, 175 | default="export.csv", 176 | ) 177 | 178 | file_group.add_argument( 179 | "--timestamp", 180 | "-t", 181 | action="store_true", 182 | dest="timestamp", 183 | help="append filename with timestamp", 184 | ) 185 | 186 | cucm_group.add_argument( 187 | "--export", 188 | "-e", 189 | action="store", 190 | dest="cucm_export", 191 | choices=["users", "phones", "translations", 192 | "sip-trunks", "registered-phones"], 193 | help="specify what you want to export", 194 | required=False, 195 | default="users", 196 | ) 197 | 198 | email_group.add_argument( 199 | "--smtpserver", 200 | "-s", 201 | action="store", 202 | dest="smtpserver", 203 | required=False, 204 | help="smtp server name or ip address", 205 | ) 206 | 207 | email_group.add_argument( 208 | "--mailto", 209 | "-m", 210 | action="store", 211 | dest="mailto", 212 | required=False, 213 | help="send output to mail recipient", 214 | ) 215 | 216 | # update variables from cli arguments 217 | cli_args = parser.parse_args() 218 | filename = cli_args.filename 219 | # print(cli_args) 220 | 221 | # store the UCM details 222 | cucm_address = cli_args.cucm_address 223 | cucm_username = cli_args.cucm_username 224 | cucm_password = cli_args.cucm_password 225 | cucm_version = cli_args.cucm_version 226 | 227 | # initialize Cisco AXL connection 228 | ucm_axl = axl( 229 | username=cucm_username, 230 | password=cucm_password, 231 | cucm=cucm_address, 232 | cucm_version=cucm_version, 233 | ) 234 | # TODO: Add RIS connection as separate credentials 235 | # ucm_ris = ris( 236 | # username=cucm_username, 237 | # password=cucm_password, 238 | # cucm=cucm_address, 239 | # cucm_version=cucm_version, 240 | # ) 241 | 242 | if cli_args.cucm_export == "users": 243 | output = cucm.export_users(ucm_axl) 244 | if len(output) > 0: 245 | saved_file = write_csv( 246 | filename=filename, cli_args=cli_args, content=output) 247 | else: 248 | print(f"status: no {cli_args.cucm_export} found...") 249 | print(f"status: elapsed time -- {datetime.now() - start_time}\n") 250 | elif cli_args.cucm_export == "phones": 251 | output = cucm.export_phones(ucm_axl) 252 | if len(output) > 0: 253 | saved_file = write_csv( 254 | filename=filename, cli_args=cli_args, content=output) 255 | else: 256 | print(f"status: no {cli_args.cucm_export} found...") 257 | print(f"status: elapsed time -- {datetime.now() - start_time}\n") 258 | elif cli_args.cucm_export == "translations": 259 | output = cucm.export_translations(ucm_axl) 260 | if len(output) > 0: 261 | saved_file = write_csv( 262 | filename=filename, cli_args=cli_args, content=output) 263 | else: 264 | print(f"status: no {cli_args.cucm_export} found...") 265 | print(f"status: elapsed time -- {datetime.now() - start_time}\n") 266 | elif cli_args.cucm_export == "sip-trunks": 267 | output = cucm.export_siptrunks(ucm_axl) 268 | if len(output) > 0: 269 | saved_file = write_csv( 270 | filename=filename, cli_args=cli_args, content=output) 271 | else: 272 | print(f"status: no {cli_args.cucm_export} found...") 273 | print(f"status: elapsed time -- {datetime.now() - start_time}\n") 274 | else: 275 | print(f"exporting {cli_args.cucm_export} is not yet supported") 276 | return 277 | 278 | # send email if selected 279 | if cli_args.mailto and cli_args.smtpserver: 280 | response = send_email( 281 | smtp_server=cli_args.smtpserver, 282 | send_to_email=cli_args.mailto, 283 | fileToSend=saved_file, 284 | ) 285 | print( 286 | f"status: mail sent to {cli_args.mailto} via {cli_args.smtpserver} at {date_time} - {saved_file}" 287 | ) 288 | elif cli_args.mailto and not cli_args.smtpserver: 289 | print(f"status: mail unable to send. no smtp server was defined") 290 | elif cli_args.smtpserver and not cli_args.mailto: 291 | print(f"status: mail unable to send. no mailto address was defined") 292 | 293 | 294 | if __name__ == "__main__": 295 | main() 296 | -------------------------------------------------------------------------------- /cucm-exporter.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | import ciscoaxl 3 | from pathlib import Path 4 | block_cipher = None 5 | 6 | a = Analysis(['cucm-exporter.py'], 7 | pathex=[Path.cwd()], 8 | binaries=[], 9 | datas=[('TEMPLATE', 'TEMPLATE'),('img', 'gooey/images'), 10 | (f'{ciscoaxl.__path__[0]}/schema/8.5/*', 'ciscoaxl/schema/8.5.'), 11 | (f'{ciscoaxl.__path__[0]}/schema/10.0/*', 'ciscoaxl/schema/10.0.'), 12 | (f'{ciscoaxl.__path__[0]}/schema/10.5/*', 'ciscoaxl/schema/10.5.'), 13 | (f'{ciscoaxl.__path__[0]}/schema/11.0/*', 'ciscoaxl/schema/11.0.'), 14 | (f'{ciscoaxl.__path__[0]}/schema/11.5/*', 'ciscoaxl/schema/11.5.'), 15 | (f'{ciscoaxl.__path__[0]}/schema/12.0/*', 'ciscoaxl/schema/12.0.'), 16 | (f'{ciscoaxl.__path__[0]}/schema/12.5/*', 'ciscoaxl/schema/12.5.'), 17 | (f'{ciscoaxl.__path__[0]}/schema/current/*', 'ciscoaxl/schema/current/.') 18 | ], 19 | hiddenimports=[], 20 | hookspath=[], 21 | runtime_hooks=[], 22 | excludes=[], 23 | win_no_prefer_redirects=False, 24 | win_private_assemblies=False, 25 | cipher=block_cipher, 26 | noarchive=False) 27 | pyz = PYZ(a.pure, a.zipped_data, 28 | cipher=block_cipher) 29 | exe = EXE(pyz, 30 | a.scripts, 31 | a.binaries, 32 | a.zipfiles, 33 | a.datas, 34 | [], 35 | name='cucm-exporter', 36 | debug=False, 37 | bootloader_ignore_signals=False, 38 | strip=False, 39 | upx=True, 40 | upx_exclude=[], 41 | runtime_tmpdir=None, 42 | # runtime_tmpdir="/", 43 | console=True, 44 | icon='img/program_icon.ico' ) 45 | -------------------------------------------------------------------------------- /cucm.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | 4 | 5 | def export_users(ucm_axl): 6 | """ 7 | retrieve users from ucm 8 | """ 9 | try: 10 | user_list = ucm_axl.get_users( 11 | tagfilter={ 12 | "userid": "", 13 | "firstName": "", 14 | "lastName": "", 15 | "directoryUri": "", 16 | "telephoneNumber": "", 17 | "enableCti": "", 18 | "mailid": "", 19 | "primaryExtension": {"pattern": "", "routePartitionName": ""}, 20 | "enableMobility": "", 21 | "homeCluster": "", 22 | "associatedPc": "", 23 | "enableEmcc": "", 24 | "imAndPresenceEnable": "", 25 | "serviceProfile": {"_value_1": ""}, 26 | "status": "", 27 | "userLocale": "", 28 | "title": "", 29 | "subscribeCallingSearchSpaceName": "", 30 | } 31 | ) 32 | all_users = [] 33 | 34 | for user in user_list: 35 | # print(user) 36 | user_details = {} 37 | user_details['userid'] = user.userid 38 | user_details['firstName'] = user.firstName 39 | user_details['lastName'] = user.lastName 40 | user_details['telephoneNumber'] = user.telephoneNumber 41 | user_details['primaryExtension'] = user.primaryExtension.pattern 42 | user_details['directoryUri'] = user.directoryUri 43 | user_details['mailid'] = user.mailid 44 | 45 | all_users.append(user_details) 46 | print( 47 | f"{user_details.get('userid')} -- {user_details.get('firstName')} {user_details.get('lastName')}: {user_details.get('primaryExtension')}" 48 | ) 49 | 50 | print("-" * 35) 51 | print(f"number of users: {len(all_users)}") 52 | # print(user_list) 53 | # print(json.dumps(all_users, indent=2)) 54 | return all_users 55 | except Exception as e: 56 | print(e) 57 | return [] 58 | 59 | 60 | def export_phones(ucm_axl): 61 | """ 62 | Export Phones 63 | """ 64 | try: 65 | phone_list = ucm_axl.get_phones( 66 | tagfilter={ 67 | "name": "", 68 | "description": "", 69 | "product": "", 70 | "model": "", 71 | "class": "", 72 | "protocol": "", 73 | "protocolSide": "", 74 | "callingSearchSpaceName": "", 75 | "devicePoolName": "", 76 | "commonDeviceConfigName": "", 77 | "commonPhoneConfigName": "", 78 | "networkLocation": "", 79 | "locationName": "", 80 | "mediaResourceListName": "", 81 | "networkHoldMohAudioSourceId": "", 82 | "userHoldMohAudioSourceId": "", 83 | "loadInformation": "", 84 | "securityProfileName": "", 85 | "sipProfileName": "", 86 | "cgpnTransformationCssName": "", 87 | "useDevicePoolCgpnTransformCss": "", 88 | "numberOfButtons": "", 89 | "phoneTemplateName": "", 90 | "primaryPhoneName": "", 91 | "loginUserId": "", 92 | "defaultProfileName": "", 93 | "enableExtensionMobility": "", 94 | "currentProfileName": "", 95 | "loginTime": "", 96 | "loginDuration": "", 97 | # "currentConfig": "", 98 | "ownerUserName": "", 99 | "subscribeCallingSearchSpaceName": "", 100 | "rerouteCallingSearchSpaceName": "", 101 | "allowCtiControlFlag": "", 102 | "alwaysUsePrimeLine": "", 103 | "alwaysUsePrimeLineForVoiceMessage": "", 104 | } 105 | ) 106 | 107 | all_phones = [] 108 | 109 | for phone in phone_list: 110 | # print(phone) 111 | phone_details = { 112 | "name": phone.name, 113 | "description": phone.description, 114 | "product": phone.product, 115 | "model": phone.model, 116 | "protocol": phone.protocol, 117 | "protocolSide": phone.protocolSide, 118 | "callingSearchSpaceName": phone.callingSearchSpaceName._value_1, 119 | "devicePoolName": phone.defaultProfileName._value_1, 120 | "commonDeviceConfigName": phone.commonDeviceConfigName._value_1, 121 | "commonPhoneConfigName": phone.commonPhoneConfigName._value_1, 122 | "networkLocation": phone.networkLocation, 123 | "locationName": phone.locationName._value_1, 124 | "mediaResourceListName": phone.mediaResourceListName._value_1, 125 | "networkHoldMohAudioSourceId": phone.networkHoldMohAudioSourceId, 126 | "userHoldMohAudioSourceId": phone.userHoldMohAudioSourceId, 127 | "loadInformation": phone.loadInformation, 128 | "securityProfileName": phone.securityProfileName._value_1, 129 | "sipProfileName": phone.sipProfileName._value_1, 130 | "cgpnTransformationCssName": phone.cgpnTransformationCssName._value_1, 131 | "useDevicePoolCgpnTransformCss": phone.useDevicePoolCgpnTransformCss, 132 | "numberOfButtons": phone.numberOfButtons, 133 | "phoneTemplateName": phone.phoneTemplateName._value_1, 134 | "primaryPhoneName": phone.primaryPhoneName._value_1, 135 | "loginUserId": phone.loginUserId, 136 | "defaultProfileName": phone.defaultProfileName._value_1, 137 | "enableExtensionMobility": phone.enableExtensionMobility, 138 | "currentProfileName": phone.currentProfileName._value_1, 139 | "loginTime": phone.loginTime, 140 | "loginDuration": phone.loginDuration, 141 | # "currentConfig": phone.currentConfig, 142 | "ownerUserName": phone.ownerUserName._value_1, 143 | "subscribeCallingSearchSpaceName": phone.subscribeCallingSearchSpaceName._value_1, 144 | "rerouteCallingSearchSpaceName": phone.rerouteCallingSearchSpaceName._value_1, 145 | "allowCtiControlFlag": phone.allowCtiControlFlag, 146 | "alwaysUsePrimeLine": phone.alwaysUsePrimeLine, 147 | "alwaysUsePrimeLineForVoiceMessage": phone.alwaysUsePrimeLineForVoiceMessage, 148 | } 149 | line_details = ucm_axl.get_phone(name=phone.name) 150 | # print(line_details.lines.line) 151 | try: 152 | for line in line_details.lines.line: 153 | # print(line) 154 | phone_details[f"line_{line.index}_dirn"] = line.dirn.pattern 155 | phone_details[f"line_{line.index}_routePartitionName"] = line.dirn.routePartitionName._value_1 156 | phone_details[f"line_{line.index}_display"] = line.display 157 | phone_details[f"line_{line.index}_e164Mask"] = line.e164Mask 158 | except Exception as e: 159 | print(e) 160 | all_phones.append(phone_details) 161 | 162 | print( 163 | f"exporting: {phone.name}: {phone.model} - {phone.description}") 164 | 165 | print("-" * 35) 166 | print(f"number of phones: {len(all_phones)}") 167 | return all_phones 168 | except Exception as e: 169 | print(e) 170 | return [] 171 | 172 | 173 | def export_siptrunks(ucm_axl): 174 | try: 175 | all_sip_trunks = [] 176 | sip_trunks = ucm_axl.get_sip_trunks( 177 | tagfilter={ 178 | "name": "", 179 | "description": "", 180 | "devicePoolName": "", 181 | "callingSearchSpaceName": "", 182 | "sipProfileName": "", 183 | "mtpRequired": "", 184 | "sigDigits": "", 185 | "destAddrIsSrv": "", 186 | } 187 | ) 188 | for siptrunk in sip_trunks: 189 | trunk = {} 190 | trunk["name"] = siptrunk.name 191 | trunk["description"] = siptrunk.description 192 | trunk["devicePoolName"] = siptrunk.devicePoolName._value_1 193 | trunk["sipProfileName"] = siptrunk.sipProfileName._value_1 194 | trunk["callingSearchSpace"] = siptrunk.callingSearchSpaceName._value_1 195 | trunk["mtpRequired"] = siptrunk.mtpRequired 196 | trunk["sigDigits"] = siptrunk.sigDigits._value_1 197 | # TODO: get_siptrunk details for destinations 198 | trunk_details = ucm_axl.get_sip_trunk(name=siptrunk.name) 199 | destinations = trunk_details['return']['sipTrunk']['destinations']['destination'] 200 | # print(destinations) 201 | for count, destination in enumerate(destinations): 202 | trunk[f'addressIpv4_{count}'] = destination.addressIpv4 203 | trunk[f'port_{count}'] = destination.port 204 | trunk[f'sortOrder_{count}'] = destination.sortOrder 205 | 206 | all_sip_trunks.append(trunk) 207 | # print(siptrunk) 208 | print(f"exporting: {siptrunk.name}: {siptrunk.description}") 209 | 210 | print("-" * 35) 211 | print(f"number of siptrunks: {len(all_sip_trunks)}") 212 | return all_sip_trunks 213 | except Exception as e: 214 | print(e) 215 | return [] 216 | 217 | 218 | def export_phone_registrations(ucm_axl, ucm_ris): 219 | """ 220 | Export Phone Registrations 221 | """ 222 | nodes = ucm_axl.list_process_nodes() 223 | del nodes[0] # remove EnterpriseWideData node 224 | subs = [] 225 | for node in nodes: 226 | subs.append(node.name) 227 | phones = ucm_axl.get_phones(tagfilter={"name": ""}) 228 | all_phones = [] 229 | phone_reg = [] 230 | reg = {} 231 | for phone in phones: 232 | all_phones.append(phone.name) 233 | 234 | def limit(all_phones, n=1000): return [ 235 | all_phones[i: i + n] for i in range(0, len(all_phones), n) 236 | ] 237 | groups = limit(all_phones) 238 | for group in groups: 239 | registered = ucm_ris.checkRegistration(group, subs) 240 | if registered["TotalDevicesFound"] < 1: 241 | print("no devices found!") 242 | else: 243 | reg["user"] = registered["LoginUserId"] 244 | reg["regtime"] = time.strftime( 245 | "%Y-%m-%d %H:%M:%S", time.localtime(registered["TimeStamp"])) 246 | for item in registered["IPAddress"]: 247 | reg["ip"] = item[1][0]["IP"] 248 | for item in registered["LinesStatus"]: 249 | reg["primeline"] = item[1][0]["DirectoryNumber"] 250 | reg["name"] = registered["Name"] 251 | print(f"exporting: {reg['name']}: {reg['ip']} - {reg['regtime']}") 252 | phone_reg.append(reg) 253 | 254 | print("-" * 35) 255 | print(f"number of registered phones: {len(phone_reg)}") 256 | return phone_reg 257 | 258 | 259 | def export_translations(ucm_axl): 260 | try: 261 | all_translations = [] 262 | translations = ucm_axl.get_translations() 263 | for translation in translations: 264 | # print(translation) 265 | xlate = {} 266 | xlate["pattern"] = translation.pattern 267 | xlate["routePartition"] = translation.routePartitionName._value_1 268 | xlate["description"] = translation.description 269 | xlate["callingSearchSpace"] = translation.callingSearchSpaceName._value_1 270 | xlate["callingPartyTransformationMask"] = translation.callingPartyTransformationMask 271 | xlate["digitDiscardInstructionName"] = translation.digitDiscardInstructionName._value_1 272 | xlate["prefixDigitsOut"] = translation.prefixDigitsOut 273 | xlate["calledPartyTransformationMask"] = translation.calledPartyTransformationMask 274 | all_translations.append(xlate) 275 | print( 276 | f"exporting: {xlate['pattern']}: {xlate['routePartition']} - {xlate['description']} --> {xlate['calledPartyTransformationMask']}") 277 | print("-" * 35) 278 | print(f"number of translations: {len(all_translations)}") 279 | return all_translations 280 | except Exception as e: 281 | return [] 282 | -------------------------------------------------------------------------------- /email_util.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import mimetypes 3 | from email.mime.image import MIMEImage 4 | from email.mime.audio import MIMEAudio 5 | from email.mime.text import MIMEText 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.base import MIMEBase 8 | from email import encoders 9 | from pathlib import Path 10 | from datetime import datetime 11 | 12 | BASE_DIR = Path(__file__).resolve().parent 13 | TEMPLATES_DIR = BASE_DIR.joinpath("TEMPLATE") 14 | 15 | 16 | def send_email(smtp_server="", send_to_email="", fileToSend="export.csv"): 17 | """ 18 | Send Email Notification 19 | """ 20 | date_time = datetime.now().strftime("%m-%d-%Y_%H.%M.%S") 21 | 22 | from_email = "noreply@presidio.com" 23 | subject = "cucm-exporter job has completed" 24 | 25 | msg = MIMEMultipart("alternative") 26 | msg["From"] = from_email 27 | msg["To"] = send_to_email 28 | msg["Subject"] = subject 29 | 30 | with open(TEMPLATES_DIR / "email-template.html", "r") as myfile: 31 | messageHTML = myfile.read().replace("\n", "").replace(r"{date_time}", date_time) 32 | 33 | messagePlain = f""" 34 | cucm-exporter has completed a job at {date_time}... 35 | 36 | Regards, 37 | The cucm admin team 38 | """ 39 | 40 | ctype, encoding = mimetypes.guess_type(fileToSend) 41 | if ctype is None or encoding is not None: 42 | ctype = "application/octet-stream" 43 | 44 | maintype, subtype = ctype.split("/", 1) 45 | 46 | if maintype == "text": 47 | fp = open(fileToSend) 48 | # Note: we should handle calculating the charset 49 | attachment = MIMEText(fp.read(), _subtype=subtype) 50 | fp.close() 51 | elif maintype == "image": 52 | fp = open(fileToSend, "rb") 53 | attachment = MIMEImage(fp.read(), _subtype=subtype) 54 | fp.close() 55 | elif maintype == "audio": 56 | fp = open(fileToSend, "rb") 57 | attachment = MIMEAudio(fp.read(), _subtype=subtype) 58 | fp.close() 59 | else: 60 | fp = open(fileToSend, "rb") 61 | attachment = MIMEBase(maintype, subtype) 62 | attachment.set_payload(fp.read()) 63 | fp.close() 64 | encoders.encode_base64(attachment) 65 | 66 | attachment.add_header("Content-Disposition", "attachment", filename=fileToSend) 67 | msg.attach(attachment) 68 | 69 | msg.attach(MIMEText(messagePlain, "plain")) 70 | msg.attach(MIMEText(messageHTML, "html")) 71 | 72 | try: 73 | server = smtplib.SMTP(smtp_server, 25) 74 | except Exception as e: 75 | return e.errno, e.strerror 76 | text = msg.as_string() 77 | try: 78 | server.sendmail(from_email, send_to_email, text) 79 | return "success" 80 | except Exception as e: 81 | return e.errno, e.strerror 82 | server.quit() 83 | 84 | 85 | if __name__ == "__main__": 86 | email_send_status = send_email( 87 | smtp_server="mail.presidio.com", 88 | send_to_email="bradh@presidio.com", 89 | fileToSend="export.csv", 90 | ) 91 | print(email_send_status) 92 | -------------------------------------------------------------------------------- /img/config_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PresidioCode/cucm-exporter/8771042afcb2f007477f7b47c1516092c621da2e/img/config_icon.png -------------------------------------------------------------------------------- /img/config_icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PresidioCode/cucm-exporter/8771042afcb2f007477f7b47c1516092c621da2e/img/config_icon2.png -------------------------------------------------------------------------------- /img/config_icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PresidioCode/cucm-exporter/8771042afcb2f007477f7b47c1516092c621da2e/img/config_icon3.png -------------------------------------------------------------------------------- /img/cucm-exporter-screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PresidioCode/cucm-exporter/8771042afcb2f007477f7b47c1516092c621da2e/img/cucm-exporter-screenshot1.png -------------------------------------------------------------------------------- /img/program_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PresidioCode/cucm-exporter/8771042afcb2f007477f7b47c1516092c621da2e/img/program_icon.ico -------------------------------------------------------------------------------- /mail/email-template.html: -------------------------------------------------------------------------------- 1 |Ready for your review...
3 | 4 |Regards,
5 |The CUCM Admin team
-------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ciscoaxl 2 | ciscoris 3 | wxPython==4.0.7.post2 4 | gooey --------------------------------------------------------------------------------