├── stuff ├── .gitignore ├── test_entity_view.py ├── vmware_tags_rest_api.py ├── get_entity_view.pl └── get_entity_views.pl ├── .ctrlpignore ├── docs ├── cmd │ ├── _index.md │ ├── list-metrics.md │ ├── media.md │ ├── about.md │ ├── vmnetdev.md │ ├── vmtools.md │ ├── snapshots.md │ ├── host-nic.md │ ├── power-state.md │ ├── host-service.md │ ├── vmguestfs.md │ ├── host-runtime.md │ ├── datastores.md │ ├── vsan.md │ ├── host-storage.md │ └── perf.md ├── README ├── _index.md └── general-options.md ├── .gitignore ├── .pre-commit-config.yaml ├── checkvsphere ├── __init__.py ├── tools │ ├── __init__.py │ ├── service_instance.py │ ├── serviceutil.py │ ├── pchelper.py │ ├── helper.py │ └── cli.py ├── vcmd │ ├── __init__.py │ ├── listmetrics.py │ ├── about.py │ ├── media.py │ ├── hostservice.py │ ├── hostnic.py │ ├── vmnetdev.py │ ├── vmguestfs.py │ ├── powerstate.py │ ├── snapshots.py │ ├── vmtools.py │ ├── datastores.py │ ├── perf.py │ ├── hoststorage.py │ ├── vsan.py │ └── hostruntime.py └── cli.py ├── .github └── workflows │ └── deploy-docs.yml ├── README.md ├── .devcontainer └── devcontainer.json ├── pyproject.toml ├── Makefile ├── CHANGES.md ├── test.sh └── LICENSE-VMWARE /stuff/.gitignore: -------------------------------------------------------------------------------- 1 | ignore/ 2 | -------------------------------------------------------------------------------- /.ctrlpignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /docs/cmd/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: commands 3 | weight: 2000 4 | --- 5 | -------------------------------------------------------------------------------- /docs/README: -------------------------------------------------------------------------------- 1 | This content is automatically managed, the origin is here: 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | test_ 3 | tmp/ 4 | .idea/ 5 | *.code-workspace 6 | build/ 7 | *.egg-info/ 8 | check_vsphere 9 | check_vsphere_* 10 | *.zip 11 | dist/ 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | -------------------------------------------------------------------------------- /checkvsphere/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1" 2 | 3 | class CheckVsphereException(Exception): 4 | pass 5 | 6 | class CheckVsphereTimeout(BaseException): 7 | pass 8 | 9 | class VsphereConnectException(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /docs/cmd/list-metrics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: list-metrics 3 | --- 4 | 5 | ## Description 6 | 7 | This command just uses the [general options](../../general-options/). It connects to the 8 | vCenter and fetches a list of 9 | [PerfCounterInfos](https://dp-downloads.broadcom.com/api-content/apis/API_VWSA_001/8.0U3/html/ReferenceGuides/vim.PerformanceManager.html#field_detail) 10 | and displays them to you, so you can make use of them with the 11 | [perf](../perf/) command. 12 | -------------------------------------------------------------------------------- /stuff/test_entity_view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from checkvsphere.tools import cli, service_instance 4 | from checkvsphere.tools.helper import find_entity_views 5 | from pyVmomi import vim 6 | 7 | parser = cli.Parser() 8 | parser.add_required_arguments(cli.Argument.NAME) 9 | args = parser.get_args() 10 | si = service_instance.connect(args) 11 | 12 | x = find_entity_views( 13 | si, 14 | vim.VirtualMachine, 15 | sieve={"name": args.name}, 16 | #properties=['runtime.powerState'] 17 | properties=['config.guestFullName','guest.hostName', 'summary'] 18 | ) 19 | -------------------------------------------------------------------------------- /docs/cmd/media.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: media 3 | --- 4 | 5 | ## Description 6 | 7 | This command reports any VMs that have floppies or cdroms attached. If it finds 8 | any the check result is CRITICAL. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | options | description | 16 | |---|---| 17 | | `--vihost VIHOST` | (optional) see [common options](../../general-options/#common-options),
only check vms on this host. If omitted all known VMs are checked | 18 | 19 | 20 | ## Examples 21 | 22 | ``` bash 23 | check_vsphere media -nossl \ 24 | -s vcenter.example.com \ 25 | -u naemon@vsphere.local \ 26 | --vihost esx1.int.example.com 27 | ``` 28 | -------------------------------------------------------------------------------- /checkvsphere/tools/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: {} 3 | push: 4 | branches: 5 | - docs 6 | 7 | jobs: 8 | copy-doc: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Deploy 13 | uses: cpina/github-action-push-to-another-repository@v1.6 14 | env: 15 | SSH_DEPLOY_KEY: ${{ secrets.OMD_CONSOL_DE_KEY }} 16 | with: 17 | source-directory: 'docs/' 18 | destination-github-username: 'ConSol' 19 | destination-repository-name: 'omd-consol-de' 20 | target-directory: 'content/en/docs/plugins/check_vsphere' 21 | commit-message: | 22 | update check_vsphere docs 23 | 24 | https://github.com/ConSol/check_vsphere/actions/workflows/deploy-docs.yml 25 | user-email: 'devnull@consol.de' 26 | user-name: '/dev/null' 27 | -------------------------------------------------------------------------------- /docs/cmd/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: about 3 | --- 4 | 5 | ### Description 6 | 7 | This command just uses the [general options](../../general-options/). It connects to the 8 | API and prints some vsphere version information. 9 | 10 | ### Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | option | description | 16 | |---|---| 17 | |`--skip-permission` | skips the System.View permission check, if omitted about check if it has System.View permissions. If it does not and `--sessionfile` is active the sessionfile is deleted if the check fails. | 18 | 19 | 20 | ### Example 21 | 22 | ``` 23 | $ check_vsphere about -s vcenter.example.com -u naemon@vsphere.local -nossl 24 | OK: VMware vCenter Server 6.7.0 build-18485185, api: VirtualCenter/6.7.3, product: VMware VirtualCenter Server 6.0 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/cmd/vmnetdev.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vm-net-dev 3 | --- 4 | 5 | ## Description 6 | 7 | This command check networkdevices of vms. 8 | 9 | ## Options 10 | 11 | Besides the [general options](../../general-options/) this command supports the following 12 | options: 13 | 14 | | options | description | 15 | |---|---| 16 | | `--mode {start-unconnected}` | check for vms that have network cards configured that are not connected on startup. `--exclude` and `--include` match against a string like "vmname;Network adapter 1" | 17 | | `--allowed REGEX` | (optional) REGEX matched against string specified in `--mode` | 18 | | `--banned REGEX` | (optional) REGEX is matched against string specified in `--mode` | 19 | 20 | 21 | ## Examples 22 | 23 | ``` bash 24 | check_vsphere vm-net-dev -nossl \ 25 | -s vcenter.example.com \ 26 | -u naemon@vsphere.local \ 27 | --mode start-unconnected 28 | ``` 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is a monitoring plugin for naemon, icinga, nagios like systems. It 4 | is meant as a successor of check\_vmware\_esx. check\_vmware\_esx is written 5 | Perl but VMWare™ has dropped support for the Perl SDK. So this plugin is written 6 | in Python using pyVmomi. 7 | 8 | ## Features 9 | 10 | The plugin has modes to check various aspects of these components: 11 | 12 | * datastores 13 | * host-runtime 14 | * host-service 15 | * host-storage 16 | * host-nic 17 | * snapshots 18 | * vsan 19 | 20 | Check the 21 | [Documentation](https://omd.consol.de/docs/plugins/check_vsphere/) 22 | for further details. 23 | 24 | # Installation 25 | 26 | ``` 27 | pip install checkvsphere 28 | ``` 29 | 30 | # LICENSE 31 | 32 | If not stated otherwise in a source file everything is licensed under 33 | GNU AFFERO GENERAL PUBLIC LICENSE Version 3. 34 | 35 | See also the LICENSE file. 36 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:0-3.6" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | "postCreateCommand": "pip3 install --user monplugin pyVmomi pre-commit", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /docs/cmd/vmtools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vm-tools 3 | --- 4 | 5 | ## Description 6 | 7 | This command reports any VMs that have VMware Tools not running. 8 | If it finds any the check result is CRITICAL. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the 13 | following options: 14 | 15 | | options | description | 16 | |---|---| 17 | | `--allowed REGEX` | (optional) REGEX is checked against ``, if REGEX doesn't match the vm is ignored | 18 | | `--banned REGEX` | (optional) REGEX is checked against ``, if REGEX does match the vm is ignored | 19 | | `--not-installed` | tools not installed is ignored by default, make them critical | 20 | | `-E EXCLUDE_GUEST_ID`, `--exclude-guest-id EXCLUDE_GUEST_ID` | if config.guestId matches, VM is ignored | 21 | 22 | ## Examples 23 | 24 | ``` bash 25 | check_vsphere vm-tools -nossl \ 26 | -s vcenter.example.com \ 27 | -u naemon@vsphere.local \ 28 | --not-installed \ 29 | -E otherGuest 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "check_vsphere" 3 | tags = [ 4 | "vsphere" 5 | ] 6 | [cascade] 7 | # github_project_repo="https://github.com/ConSol/check_vsphere" 8 | # github_repo="https://github.com/ConSol/check_vsphere" 9 | # github_subdir="docs" 10 | # github_branch="main" 11 | # path_base_for_github_subdir="content/en/docs/plugins/check_vsphere/" 12 | +++ 13 | 14 | This is a monitoring plugin for naemon, nagios or icinga compatible systems that 15 | can check various things against the vSphere API. It is meant as a replacement 16 | for `check_vmware_esx`, `check_vmware_api` and the like. These are all written 17 | in Perl. This rewrite is needed because the Perl SDK for the vSphere API was 18 | [marked deprecated](https://developer.broadcom.com/sdks?tab=Compute%2520Virtualization) by VMware. 19 | 20 | {{% alert title="Note" color="warning" %}} 21 | It's **not** a drop in replacement for any of the tools mentioned above. 22 | {{% /alert %}} 23 | 24 | The only supported programming alternatives are Java and Python. We chose python 25 | for the implementation. 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "flit_core.buildapi" 3 | requires = ["flit_core >=3.2,<4"] 4 | 5 | [project] 6 | name = "checkvsphere" 7 | readme = "README.md" 8 | description = "check_vsphere monitoring plugin" 9 | version = "0.3.20" 10 | requires-python = ">= 3.6" 11 | authors = [ 12 | { name = "Danijel Tasov", email = "danijel.tasov@consol.de" } 13 | ] 14 | dependencies = [ 15 | "pyvmomi >= 8.0.3.0.1, < 10", 16 | "monplugin >= 0.6.1", 17 | ] 18 | classifiers = [ 19 | "Development Status :: 3 - Alpha", 20 | "Topic :: System :: Monitoring", 21 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", 22 | ] 23 | 24 | [project.scripts] 25 | check_vsphere = "checkvsphere.cli:main" 26 | 27 | [project.urls] 28 | "homepage" = "https://github.com/consol/check_vsphere" 29 | "documentation" = "https://consol.github.io/check_vsphere" 30 | "repository" = "https://github.com/consol/check_vsphere.git" 31 | "issues" = "https://github.com/consol/check_vsphere/issues" 32 | 33 | [tool.flit.sdist] 34 | exclude = ["docs/", "stuff/", "dist/", ".ruff_cache", "inkubator"] 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean test 2 | 3 | PYTHON=python3 4 | 5 | all: check_vsphere_bundle check_vsphere 6 | 7 | check_vsphere_bundle: 8 | pip install --no-cache-dir --no-compile --target allinone . 9 | mv allinone/bin/check_vsphere allinone/__main__.py 10 | $(PYTHON) -m zipapp -c -p '/usr/bin/env python3' allinone 11 | rm -rf allinone 12 | mv allinone.pyz check_vsphere_bundle 13 | 14 | check_vsphere: 15 | mkdir build 16 | cp -av checkvsphere build/checkvsphere 17 | mv build/checkvsphere/cli.py build/__main__.py 18 | ( cd build/; $(PYTHON) -m zipapp -c --output ../check_vsphere -p '/usr/bin/env python3' . ) 19 | rm -rf build 20 | 21 | dist: pyproject.toml 22 | $(PYTHON) -m build 23 | chmod a+r dist/* 24 | 25 | .PHONY: clean 26 | clean: 27 | rm -rf build allinone check_vsphere_bundle check_vsphere zip check_vsphere.zip build check_vsphere.egg-info dist 28 | 29 | .PHONY: upload-test 30 | upload-test: dist 31 | $(PYTHON) -m twine upload --repository testpypi dist/* 32 | 33 | .PHONY: upload-prod 34 | upload-prod: dist 35 | $(PYTHON) -m twine upload dist/* 36 | 37 | .PHONY: upload-private 38 | upload-private: dist 39 | rsync -avH dist/* cb:~/public_html/simple/checkvsphere 40 | -------------------------------------------------------------------------------- /docs/cmd/snapshots.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: snapshots 3 | --- 4 | 5 | ## Description 6 | 7 | This command checks VirtualMachine snapshots by age or count. 8 | 9 | ## Options 10 | 11 | Besides the [general options](../../general-options/) this command supports the following 12 | options: 13 | 14 | | options | description | 15 | |---|---| 16 | | `--mode {age,count}` | (required) thresholds checked against the `age` of a snapshot or against the `count` of the snapshots by VirtualMachine | 17 | | `--allowed REGEX` | (optional) REGEX is checked against `;`, if REGEX doesn't match the snapshot is ignored | 18 | | `--banned REGEX` | (optional) REGEX is checked against `;`, if REGEX does match the snapshot is ignored | 19 | | `--critical CRITICAL` | critical threshold, see [common options](../../general-options/#common-options) | 20 | | `--warning WARNING` | warning threshold, see [common options](../../general-options/#common-options) | 21 | 22 | 23 | ## Examples 24 | 25 | ``` bash 26 | # notify if snapshots are too old 27 | check_vsphere snapshots -nossl \ 28 | -s vcenter.example.com \ 29 | -u naemon@vsphere.local \ 30 | --mode age \ 31 | --warning 150 \ 32 | --critical 180 \ 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/cmd/host-nic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: host-nic 3 | --- 4 | 5 | ## Description 6 | 7 | The host-nic command checks if all nics are connected. It's basically the same 8 | as `--select net --subselect=nic` from check\_vmware\_esx against a host. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | option | description | 16 | |---|---| 17 | | `--vihost HOSTNAME` | (optional) the name of the HostSystem to check, if omitted the first HostSystem found is checked, which is handy if you run this check directly against the host | 18 | | `--maintenance-state STATE` | one of OK, WARNING, CRITICAL, UNKNOWN. The status to use when the host is in maintenance mode, this defaults to UNKNOWN except when --mode maintenance, then the default is CRITICAL | 19 | | `--unplugged_state STATE` | one of OK, WARNING, CRITICAL. The status to use when a nic is in plugged state, defaults to WARNING | 20 | | `--banned REGEX` | all matching nics matching this REGEXP are ignores, can be used multiple times | 21 | 22 | ## Examples 23 | 24 | ``` 25 | check_vsphere host-nic \ 26 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 27 | --vihost esx-2.example.com --mode status 28 | OK: All nics connected 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/cmd/power-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: power-state 3 | --- 4 | 5 | ## Description 6 | 7 | This command checks the powerState of all hosts in a vcenter. 8 | 9 | ## Options 10 | 11 | Besides the [general options](../../general-options/) this command supports the following 12 | options: 13 | 14 | | options | description | 15 | |---|---| 16 | | `--allowed REGEX` | (optional) REGEX is checked against ``, if REGEX doesn't match the host is ignored | 17 | | `--banned REGEX` | (optional) REGEX is checked against ``, if REGEX does match the host is ignored | 18 | | `--cluster-name CLUSTERNAME` | (optional) consider only hosts in cluster CLUSTERNAME | 19 | | `--metric METRIC` | One of total, up, down, ignored, up%, down% | which metric to apply THRESHOLD on (default is `down`) | 20 | | `--warning THRESHOLD` | (optional) warning threshold, see [common options](../../general-options/#common-options) | 21 | | `--critical THRESHOLD` | (optional) critical threshold, see [common options](../../general-options/#common-options) | 22 | 23 | if no thresholds are given the exit the check is basically equal to 24 | `power-state --critical 1 --metric down`. Just the output is a bit different. 25 | 26 | ## Examples 27 | 28 | ``` 29 | $ check_vsphere power-state \ 30 | -s vcenter.example.com \ 31 | -u naemon@vsphere.local 32 | OK: 10 hosts, 0 ignored, unpowered 0 33 | All hosts ok 34 | | 'hosts'=10.0;;;; 35 | 'ignored hosts'=0.0;;;; 36 | 'hosts with power'=10.0;;;; 37 | 'monplugin_time'=0.058525s 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/cmd/host-service.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: host-service 3 | --- 4 | 5 | ## Description 6 | 7 | The host-service checks the services are running. Since you probably don't have 8 | all of them running you probably want to filter the ones to check with the 9 | `--allowed` parameter. 10 | 11 | ## Options 12 | 13 | Besides the [general options](../../general-options/) this command supports the following 14 | options: 15 | 16 | | option | description | 17 | |---|---| 18 | | `--vihost HOSTNAME` | (optional) the name of the HostSystem to check, if omitted the first HostSystem found is checked, which is handy if you run this check directly against the host | 19 | | `--maintenance-state STATE` | one of OK, WARNING, CRITICAL, UNKNOWN. The status to use when the host is in maintenance mode, this defaults to UNKNOWN | 20 | | `--allowed REGEX` | (optional) REGEX is checked against ``, if REGEX doesn't match the service is ignored | 21 | | `--banned REGEX` | (optional) REGEX is checked against ``, if REGEX does match the service is ignored | 22 | 23 | 24 | ## Examples 25 | 26 | ``` 27 | $ check_vsphere host-services \ 28 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 29 | --vihost esx-2.example.com --allowed 'vpxa|ntpd|DCUI' 30 | OK: running: 3; not running: 0 31 | DCUI running 32 | ntpd running 33 | vpxa running 34 | ``` 35 | 36 | ``` 37 | $ check_vsphere host-services \ 38 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 39 | --vihost esx-2.example.com --allowed 'snmpd' 40 | CRITICAL: running: 3; not running: 1 41 | snmpd not running 42 | DCUI running 43 | ntpd running 44 | vpxa running 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/cmd/vmguestfs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vm-guestfs 3 | --- 4 | 5 | ## Description 6 | 7 | This command can check disk usage of mounted filesystems in a 8 | VM if vmwaretools are running. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the 13 | following options: 14 | 15 | | options | description | 16 | |---|---| 17 | | `--vm-name VMNAME` | name of the VM to check | 18 | | `--warning THRESHOLD` | (optional) critical threshold, see [common options](../../general-options/#common-options) | 19 | | `--critical THRESHOLD` | (optional) critical threshold, see [common options](../../general-options/#common-options) | 20 | | `--allowed REGEX` | (optional) REGEX is checked against ``, if REGEX doesn't match the fs is ignored | 21 | | `--banned REGEX` | (optional) REGEX is checked against ``, if REGEX does match the fs is ignored | 22 | | `--match-method [search,match,fullmatch]` | see [common options](../../general-options/#common-options) | 23 | | `--metric METRIC` | the name of the metric to check, can be one of `usage`, `used`, `free`.
`used` and `free` can be suffixed by a unit (B, kB, MB, GB), like `free_MB` or `used_GB`.
if omitted it defaults to `usage`, which is (100 * used/capacity) | 24 | 25 | 26 | ## Examples 27 | 28 | ``` bash 29 | # check all filesystems mounted under /var (/var /var/tmp /var/log etc) 30 | # if they have less than 5GB of free space 31 | check_vsphere vm-guestfs -nossl \ 32 | -s vcenter.example.com \ 33 | -u naemon@vsphere.local \ 34 | --vm-name examplevm \ 35 | --metric free_GB \ 36 | --critical 5: \ 37 | --match-method match \ 38 | --allow /var 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/general-options.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: General Options 3 | weight: 100 4 | --- 5 | 6 | | option | description | 7 | |---|---| 8 | | `-s HOST, --host HOST` | vSphere service address to connect to | 9 | | `-o PORT, --port PORT` | Port to connect on | 10 | | `-u USER, --user USER` | User name to use when connecting to host | 11 | | `-p PASSWORD, --password` | Password to use when connecting to host, can also be set by env `VSPHERE_PASS` | 12 | | `-nossl, --disable-ssl-verification` | Disable ssl host certificate verification | 13 | | `--sessionfile FILE` | it caches sessionId in FILE to avoid logging in and out so much | 14 | 15 | # Common Options 16 | 17 | | option | description | 18 | |---|---| 19 | | `--vihost VIHOST` | name of the ESXi Host as seen by the vCenter | 20 | | `--vimtype VIMTYPE` | the object type to check,
it's a [managed entity](https://dp-downloads.broadcom.com/api-content/apis/API_VWSA_001/8.0U3/html/ReferenceGuides/vim.ManagedEntity.html) like: HostSystem, Datacenter or VirtualMachine | 21 | | `--vimname VIMNAME` | the name of the ManagedEntity of vimtype | 22 | | `--warning WARNING` | warning [threshold](https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT) | 23 | | `--critical CRITICAL` | critical [threshold](https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT) | 24 | | `--match-method search,match,fullmatch` | Some commands have `--allowed` or `--banned` options, which accept a REGEX as an argument. This option modifies the behavior of the regex. For more information, refer to Python's re.search, re.match, and re.fullmatch documentation. The default value is 'search'.| 25 | 26 | # Environment Variables 27 | 28 | | var | description | 29 | |---|---| 30 | | `CONNECT_NOFAIL` | if set a connection error exits with status OK | 31 | | `TIMEOUT` | Global timeout of the plugin in seconds, defaults to `30` | 32 | | `VSPHERE_PASS` | default value for `--password` option | 33 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # v0.3.18 2 | 3 | * add vm-guestfs command 4 | * add `--match-method` option that can change the behavior of the REGEX passed to 5 | `--allowed` or `--banned` 6 | 7 | # v0.3.17 8 | 9 | * improve vm-tools some more, VMS can be ignores by config.guestId 10 | 11 | # v0.3.16 12 | 13 | * improve vm-tools: add --vihost argument to limit checks to a ESX host 14 | and polish output a bit 15 | 16 | # v0.3.15 17 | 18 | * perf: report an error if `--perfinstance '*'` is used but the metric 19 | just has an aggregate available 20 | 21 | # v0.3.14 22 | 23 | * report warning if we miss datastores we want to check 24 | 25 | # v0.3.13 26 | 27 | * add vm-tools subcommand that checks for VMs that don't have vm-tools installed 28 | @BenjaminBoehm #25 29 | 30 | # v0.3.12 31 | 32 | * add `--mode path` to host-storage 33 | 34 | # v0.3.11 35 | 36 | * [BREAKING] add check for System.View permission to about cmd. use 37 | `--skip-permission` to skip this (old behavior). 38 | 39 | # v0.3.10 40 | 41 | * fix error in media with offline vms 42 | 43 | # v0.3.9 44 | 45 | * make use of sessionId support in pyVmomi for `--sessionfile` 46 | 47 | # v0.3.8 48 | 49 | * depend on 8.0.3.0.1 because of the vsan command, the extra non free bindings 50 | are now included in pyVmomi and don't exist anymore as a separate project 51 | 52 | # v0.3.7 53 | 54 | * limit number in plugin output to 8 significant digits 55 | 56 | # v0.3.6 57 | 58 | * add experimental sessionfile support (MLUECKERT) 59 | 60 | # v0.3.5 61 | 62 | * datastores: add support for vimtype Datacenter (MLUECKERT) 63 | 64 | # v0.3.4 65 | 66 | * add vm-net-dev check 67 | 68 | # v0.3.3 69 | 70 | * adjust object status mapping in vsan cmd 71 | 72 | # v0.3.2 73 | 74 | * fix status message in hostruntime 75 | 76 | # v0.3.1 77 | 78 | * Try harder to extract a nice message from the exceptions if CONNECT_NOFAIL is set 79 | 80 | # v0.3.0 81 | 82 | * BREAKING CHANGE: avoid check_multi compatible performance data. pnp4nagios has it's issues with that 83 | * improve powerstate 84 | 85 | # v0.2.3 86 | 87 | * host-runtime - improved performance 88 | -------------------------------------------------------------------------------- /docs/cmd/host-runtime.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: host-runtime 3 | --- 4 | 5 | ## Description 6 | 7 | The host-runtime provides various checks against a hosts runtime. It's basically 8 | the same as `--select runtime` from check\_vmware\_esx against a host. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | option | description | 16 | |---|---| 17 | | `--vihost HOSTNAME` | (optional) the name of the HostSystem to check, if omitted the first HostSystem found is checked, which is handy if you run this check directly against the host | 18 | | `--mode MODE` | what to check to perform, one of `con`, `health`, `issues`, `status`, `temp`, `version`, `maintenance` | 19 | | `--maintenance-state STATE` | one of OK, WARNING, CRITICAL, UNKNOWN. The status to use when the host is in maintenance mode, this defaults to UNKNOWN except when --mode maintenance, then the default is CRITICAL | 20 | 21 | ## Examples 22 | 23 | ``` 24 | check_vsphere host-runtime \ 25 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 26 | --vihost esx-2.example.com --mode status 27 | CRITICAL: overall status is RED 28 | ``` 29 | 30 | ``` 31 | check_vsphere host-runtime \ 32 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 33 | --vihost esx-2.example.com --mode con 34 | OK: connection state is 'connected' 35 | ``` 36 | 37 | ``` 38 | check_vsphere host-runtime \ 39 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 40 | --vihost esx-2.example.com --mode health 41 | OK: All 53 health checks are GREEN: memory: 10, storage: 9, other: 1, voltage: 12, fan: 4, temperature: 15, power: 2 42 | ``` 43 | 44 | ``` 45 | check_vsphere host-runtime \ 46 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 47 | --vihost esx-2.example.com --mode status 48 | CRITICAL: overall status is RED 49 | ``` 50 | 51 | ``` 52 | check_vsphere host-runtime \ 53 | -s vcenter.example.com -u naemon@vsphere.local -nossl \ 54 | --vihost esx-2.example.com --mode temp 55 | OK: All temperature sensors green 56 | | 'Memory Device 77 DIMMD2'=43.0;;;; ... 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/cmd/datastores.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: datastores 3 | --- 4 | 5 | ## Description 6 | 7 | This command checks remaining capacity on the datastores, and if they are 8 | accessible. This is known as `--select volumes` from check\_vmware\_esx. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | options | description | 16 | |---|---| 17 | | `--allowed REGEX` | (optional) REGEX is checked against ``, if REGEX doesn't match the snapshot is ignored | 18 | | `--banned REGEX` | (optional) REGEX is checked against ``, if REGEX does match the snapshot is ignored | 19 | | `--vimtype VIMTYPE` | the object type to check, see [common options](../../general-options/#common-options), currently HostSystem and ClusterComputeResource are supported here, if omitted all datastores are checked | 20 | | `--vimname VIMNAME` | name of the vimtype object, see [common options](../../general-options/#common-options) | 21 | | `--critical CRITICAL` | critical threshold, see [common options](../../general-options/#common-options) | 22 | | `--warning WARNING` | warning threshold, see [common options](../../general-options/#common-options) | 23 | | `--metric METRIC` | the name of the metric to check, can be one of `usage`, `used`, `free`.
`used` and `free` can be suffixed by a unit (B, kB, MB, GB), like `free_MB` or `used_GB`.
if omitted it defaults to `usage`, which is (100 * used/capacity) | 24 | 25 | In case of `--vimtype HostSystem` it may be useful to omit the `--vimname` when 26 | you run this command directly against the HostSystem (not through the vcenter). 27 | 28 | ## Examples 29 | 30 | ``` bash 31 | # notify volumes that have less than 10GB left 32 | check_vsphere datastores -nossl \ 33 | -s vcenter.example.com \ 34 | -u naemon@vsphere.local \ 35 | --metric free_GB \ 36 | --critical 10: \ 37 | --vimtype HostSystem \ 38 | --vimname esx1.example.com 39 | ``` 40 | 41 | ``` bash 42 | # notify volumes that have a usage of 90% 43 | check_vsphere datastores -nossl \ 44 | -s vcenter.example.com \ 45 | -u naemon@vsphere.local \ 46 | --critical 90 47 | ``` 48 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/listmetrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | print all metric counters 21 | """ 22 | 23 | __cmd__ = 'list-metrics' 24 | 25 | import textwrap 26 | from ..tools import cli, service_instance 27 | 28 | def run(): 29 | parser = cli.Parser() 30 | args = parser.get_args() 31 | si = service_instance.connect(args) 32 | perfMgr = si.content.perfManager 33 | 34 | metrics = {} 35 | 36 | for counter in perfMgr.perfCounter: 37 | group = counter.groupInfo.key 38 | name = counter.nameInfo.key 39 | rollup = counter.rollupType 40 | 41 | metrics.setdefault(str(group), {}) \ 42 | .setdefault(str(name), {}) \ 43 | .setdefault(str(rollup), counter) 44 | 45 | for group in metrics: 46 | for name in metrics[group]: 47 | for rollup in metrics[group][name]: 48 | counter = metrics[group][name][rollup] 49 | print("{:4d} {}:{}:{} ({} [{}])\n{}\n".format( 50 | counter.key, group, name, rollup, 51 | counter.unitInfo.summary, counter.unitInfo.key, 52 | textwrap.fill( 53 | counter.nameInfo.summary, 54 | width=72, 55 | initial_indent=' ', 56 | subsequent_indent=' ') 57 | )) 58 | 59 | 60 | if __name__ == "__main__": 61 | run() 62 | -------------------------------------------------------------------------------- /docs/cmd/vsan.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vsan 3 | --- 4 | 5 | ## Description 6 | 7 | The vsan command provides checks against the vSAN system of a vcenter. Host 8 | endpoints are currently not supported. 9 | 10 | ## Options 11 | 12 | Besides the [general options](../../general-options/) this command supports the following 13 | options: 14 | 15 | | option | description | 16 | |---|---| 17 | | `--vihost HOSTNAME` | (optional) the name of the HostSystem to check, if omitted the first HostSystem found is checked, which is handy if you run this check directly against the host | 18 | | `--maintenance-state STATE` | one of OK, WARNING, CRITICAL, UNKNOWN. The status to use when the host is in maintenance mode, this defaults to UNKNOWN | 19 | | `--mode MODE` | one of objecthealth, healthtest | 20 | | `--include REGEX` | (optional) REGEX is checked against the cluster name | 21 | | `--exclude REGEX` | (optional) REGEX is checked against the cluster name | 22 | | `--include-group REGEX` | (optional) only with `--mode healthtest`, REGEX is checked against the tests' group name | 23 | | `--include-test REGEX` | (optional) only with `--mode healthtest`, REGEX is checked against the test name | 24 | | `--exclude-group REGEX` | (optional) only with `--mode healthtest`, REGEX is checked against the tests' group name | 25 | | `--exclude-test REGEX` | (optional) only with `--mode healthtest`, REGEX is checked against the test name | 26 | | `--cache` | fetch cached data from the API when available and not outdated | 27 | | `--verbose` | show also tests the where OK | 28 | 29 | ### `--mode healthtest` 30 | 31 | This corresponds to the following in the vcenter: 32 | 33 | If you navigate to Cluster/Monitor/vSAN/Skyline Health you will see a sidebar 34 | with items like "Hardware compatibility", "Online Health" and so on. These are 35 | the several group of tests (you can ignore a whole groups with 36 | `--exclude-group/--include-group`) 37 | 38 | You can expand them and see the individual names of each test. These can be 39 | ignored as well (`--exclude-test/--include-test`). 40 | 41 | ### `--mode objecthealth` 42 | 43 | REGEX of `--include`, `--exclude` is matched against cluster name. 44 | 45 | This is an in depth check of the "vSAN object health" test. It's not very well 46 | tested yet. 47 | 48 | ## Examples 49 | 50 | ``` 51 | $ check_vsphere vsan \ 52 | -s vcenter.example.com -u naemon@vsphere.local \ 53 | -m healthtest --include 'Cluster 1' 54 | ``` 55 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/about.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | Connects to the API and prints the version 21 | """ 22 | 23 | __cmd__ = 'about' 24 | 25 | import logging 26 | import os 27 | from monplugin import Status 28 | from pyVmomi import vim 29 | from ..tools import cli, service_instance 30 | 31 | 32 | def run(): 33 | try: 34 | parser = cli.Parser() 35 | parser.add_optional_arguments({ 36 | 'name_or_flags': ['--skip-permission'], 37 | 'options': { 38 | 'action': 'store_true', 39 | 'default': False, 40 | 'help': 'skips the System.View permission check', 41 | } 42 | }) 43 | 44 | args = parser.get_args() 45 | si = service_instance.connect(args) 46 | about = si.content.about 47 | status = Status.OK 48 | clock = True 49 | if not args.skip_permission: 50 | try: 51 | clock = si.serverClock 52 | except Exception: 53 | logging.debug("no server clock", exc_info=1) 54 | status = Status.CRITICAL 55 | clock = None 56 | if args.sessionfile: 57 | try: 58 | logging.debug(f"deleting {args.sessionfile}") 59 | os.unlink(args.sessionfile) 60 | except Exception: 61 | logging.debug(f"unlink {args.sessionfile} failed", exc_info=1) 62 | 63 | out = ( 64 | f'{status.name}: ' 65 | f'{ "No System.View permission, " if not clock else "" }' 66 | f'{ about.fullName }, ' 67 | f'api: { about.apiType }/{ about.apiVersion }, ' 68 | f'product: { about.licenseProductName } { about.licenseProductVersion }' 69 | ) 70 | print(out) 71 | raise SystemExit(status.value) 72 | except vim.fault.VimFault as e: 73 | if hasattr(e, 'msg'): 74 | print(f"ERROR: {e.msg}") 75 | else: 76 | print(f"ERROR: {e}") 77 | raise SystemExit(2) 78 | except Exception as e: 79 | print(f"ERROR: {e}") 80 | raise SystemExit(2) 81 | 82 | if __name__ == "__main__": 83 | run() 84 | -------------------------------------------------------------------------------- /checkvsphere/tools/service_instance.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements simple helper functions for managing service instance objects 3 | 4 | """ 5 | 6 | # VMware vSphere Python SDK Community Samples Addons 7 | # Copyright (c) 2014-2021 VMware, Inc. All Rights Reserved. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | # See LICENSE-VMWARE 22 | 23 | __author__ = "VMware, Inc." 24 | 25 | import atexit 26 | import logging 27 | import os 28 | from pyVim.connect import SmartConnect, Disconnect 29 | from .. import VsphereConnectException 30 | 31 | 32 | def write_session_id(service_instance, sessionfile): 33 | with open(sessionfile, "w") as s: 34 | logging.debug(f'saving session to {sessionfile}') 35 | s.write(service_instance._GetStub().GetSessionId()) 36 | 37 | 38 | def read_session_id(sessionfile): 39 | try: 40 | logging.debug(f'read session from {sessionfile}') 41 | return open(sessionfile).read() 42 | except FileNotFoundError: 43 | return None 44 | except: 45 | logging.exception("Error restoring session") 46 | return None 47 | 48 | def connect(args): 49 | """ 50 | Determine the most preferred API version supported by the specified server, 51 | then connect to the specified server using that API version, login and return 52 | the service instance object. 53 | """ 54 | sessionfile = args.sessionfile 55 | sessionId = None 56 | service_instance = None 57 | if sessionfile: 58 | sessionId = read_session_id(args.sessionfile) 59 | 60 | 61 | params = { 62 | "host": args.host, 63 | "port": args.port, 64 | "pwd": args.password, 65 | "user": args.user, 66 | "disableSslCertValidation": bool(args.disable_ssl_verification), 67 | "sessionId": sessionId, 68 | } 69 | 70 | try: 71 | try: 72 | service_instance = SmartConnect(**params) 73 | except Exception: 74 | if sessionId: 75 | logging.debug("retry without sessionId") 76 | del params["sessionId"] 77 | service_instance = SmartConnect(**params) 78 | else: 79 | raise 80 | except Exception as e: 81 | if os.environ.get("CONNECT_NOFAIL", None): 82 | raise VsphereConnectException("cannot connect") from e 83 | else: 84 | raise e 85 | 86 | if sessionfile: 87 | write_session_id(service_instance, args.sessionfile) 88 | else: 89 | logging.debug('add disconnect handler') 90 | # doing this means you don't need to remember to disconnect your script/objects 91 | atexit.register(Disconnect, service_instance) 92 | 93 | return service_instance 94 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | p=${PYTHON:-$HOME/.virtualenvs/pyvmomi/bin/python} 2 | OPTS=(-nossl -s 127.0.0.1 -o 8989 -u user -p pass) 3 | 4 | check() { 5 | local code=$1; shift; 6 | local cmd=$1; shift; 7 | local out 8 | local cmd=("$p" -m checkvsphere.cli $cmd "${OPTS[@]}" "$@") 9 | out=$("${cmd[@]}") 10 | local r=$? 11 | if [ $r -ne $code ] ; then 12 | echo "failed: ${cmd[*]}" 13 | echo "got $r" 14 | echo "expected $code" 15 | printf '%s\n' "$out" 16 | exit 17 | fi 18 | } 19 | 20 | check 0 about 21 | check 2 datastores --allowed ftpint --warning '80:' 22 | 23 | check 0 snapshots --mode age --warning 0 --banned 'wazuh|test|roc|cm2' 24 | check 1 snapshots --mode age --warning 1 25 | check 0 snapshots --mode age --warning 10000 26 | check 1 snapshots --mode count --warning 1 27 | check 0 snapshots --mode count --warning 10000 28 | check 2 snapshots --mode age --critical 1 29 | check 0 snapshots --mode age --critical 10000 30 | check 2 snapshots --mode count --critical 1 31 | check 0 snapshots --mode count --critical 10000 32 | check 1 snapshots --mode count --warning 1 33 | check 0 snapshots --mode count --warning 5 34 | 35 | check 2 media 36 | check 0 media --banned 'dc1|mbd|roc|azubi|target2' 37 | check 0 power-state 38 | check 0 hostnic --vihost esx-int-13.m.consol.de 39 | check 0 hostruntime --vihost esx-int-13.m.consol.de --mode=health 40 | 41 | # hoststorage and hostservice don't work with the simulator :( 42 | 43 | check 0 perf --vimtype HostSystem --vimname esx-int-12.m.consol.de --perfcounter cpu:usage:average --maintenance-state=OK 44 | check 1 perf --vimtype HostSystem --vimname esx-int-12.m.consol.de --perfcounter cpu:usage:average --maintenance-state=WARNING 45 | check 2 perf --vimtype HostSystem --vimname esx-int-12.m.consol.de --perfcounter cpu:usage:average --maintenance-state=CRITICAL 46 | check 3 perf --vimtype HostSystem --vimname esx-int-12.m.consol.de --perfcounter cpu:usage:average --maintenance-state=UNKNOWN 47 | check 3 perf --vimtype HostSystem --vimname esx-int-12.m.consol.de --perfcounter cpu:usage:average 48 | check 0 perf --vimtype HostSystem --vimname esx-int-13.m.consol.de --perfcounter cpu:usage:average 49 | 50 | check 0 hoststorage --vihost esx-int-12.m.consol.de --maintenance-state=OK --mode=lun 51 | check 1 hoststorage --vihost esx-int-12.m.consol.de --maintenance-state=WARNING --mode=lun 52 | check 2 hoststorage --vihost esx-int-12.m.consol.de --maintenance-state=CRITICAL --mode=lun 53 | check 3 hoststorage --vihost esx-int-12.m.consol.de --maintenance-state=UNKNOWN --mode=lun 54 | check 3 hoststorage --vihost esx-int-12.m.consol.de --mode=lun 55 | 56 | check 0 hostservice --vihost esx-int-12.m.consol.de --maintenance-state=OK 57 | check 1 hostservice --vihost esx-int-12.m.consol.de --maintenance-state=WARNING 58 | check 2 hostservice --vihost esx-int-12.m.consol.de --maintenance-state=CRITICAL 59 | check 3 hostservice --vihost esx-int-12.m.consol.de --maintenance-state=UNKNOWN 60 | check 3 hostservice --vihost esx-int-12.m.consol.de 61 | 62 | check 0 hostruntime --vihost esx-int-12.m.consol.de --maintenance-state=OK --mode=health 63 | check 1 hostruntime --vihost esx-int-12.m.consol.de --maintenance-state=WARNING --mode=health 64 | check 2 hostruntime --vihost esx-int-12.m.consol.de --maintenance-state=CRITICAL --mode=health 65 | check 3 hostruntime --vihost esx-int-12.m.consol.de --maintenance-state=UNKNOWN --mode=health 66 | check 3 hostruntime --vihost esx-int-12.m.consol.de --mode=health 67 | 68 | check 0 hostnic --vihost esx-int-12.m.consol.de --maintenance-state=OK 69 | check 1 hostnic --vihost esx-int-12.m.consol.de --maintenance-state=WARNING 70 | check 2 hostnic --vihost esx-int-12.m.consol.de --maintenance-state=CRITICAL 71 | check 3 hostnic --vihost esx-int-12.m.consol.de --maintenance-state=UNKNOWN 72 | check 3 hostnic --vihost esx-int-12.m.consol.de 73 | -------------------------------------------------------------------------------- /docs/cmd/host-storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: host-storage 3 | --- 4 | 5 | ## Description 6 | 7 | The host-storage command provides various checks against a hosts storage system. 8 | It's basically the same as `--select storage` from check\_vmware\_esx against a 9 | host. 10 | 11 | ## Options 12 | 13 | Besides the [general options](../../general-options/) this command supports the following 14 | options: 15 | 16 | | option | description | 17 | |---|---| 18 | | `--vihost HOSTNAME` | (optional) the name of the HostSystem to check, if omitted the first HostSystem found is checked, which is handy if you run this check directly against the host | 19 | | `--maintenance-state STATE` | one of OK, WARNING, CRITICAL, UNKNOWN. The status to use when the host is in maintenance mode, this defaults to UNKNOWN | 20 | | `--mode MODE` | one of adapter, lun, path | 21 | | `--allowed REGEX` | (optional) REGEX is checked against a name depending on the `--mode` | 22 | | `--banned REGEX` | (optional) REGEX is checked against a name depending on the `--mode` | 23 | 24 | On `--mode adapter` REGEX is matched against device name, the model or the device-key of the adapter. 25 | On `--mode lun` REGEX is matched against displayName of the scsi device 26 | 27 | ## Examples 28 | 29 | ``` 30 | $ check_vsphere host-storage -u naemon@vsphere.local -s vcenter.example.com --vihost esx-1.example.com 31 | OK: LUNs: 12; ok: 12 32 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae650000000000000116) state: ok 33 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b0) state: ok 34 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b2) state: ok 35 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b4) state: ok 36 | OK LUN:000 AVAGO Serial Attached SCSI Disk (naa.600605b00ba8d09022672ce36caf54d0) state: ok 37 | OK LUN:000 AVAGO Serial Attached SCSI Disk (naa.600605b00ba8d090203f049d28bd7587) state: ok 38 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000fa) state: ok 39 | OK LUN:000 AVAGO Serial Attached SCSI Disk (naa.600605b00ba8d090203f04c62b3418e7) state: ok 40 | OK LUN:000 Local ATA Disk (t10.ATA_____INTEL_SSDSC2BX200G4_____________________BTHC714504MK200TGN__) state: ok 41 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000ae) state: ok 42 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000ee) state: ok 43 | OK LUN:000 AVAGO Serial Attached SCSI Disk (naa.600605b00ba8d090203f05032ece21fb) state: ok 44 | ``` 45 | 46 | ``` 47 | $ check_vsphere host-storage -u naemon@vsphere.local -s vcenter.example.com \ 48 | --vihost esx-1.example.com --allowed LEFTHAND 49 | OK: LUNs: 12; ignored: 5; ok: 7 50 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae650000000000000116) state: ok 51 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b0) state: ok 52 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b2) state: ok 53 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000b4) state: ok 54 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000fa) state: ok 55 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000ae) state: ok 56 | OK LUN:000 LEFTHAND iSCSI Disk (naa.6000eb34b50dae6500000000000000ee) state: ok 57 | ``` 58 | 59 | ``` 60 | $ check_vsphere host-storage -u naemon@vsphere.local -s vcenter.example.com \ 61 | --vihost esx1.example.com --mode adapter 62 | CRITICAL: Adapters 4; online: 1; unknown: 3 63 | Wellsburg AHCI Controller vmhba0 (unknown) 64 | Wellsburg AHCI Controller vmhba1 (unknown) 65 | MegaRAID SAS Invader Controller vmhba2 (unknown) 66 | iSCSI Software Adapter vmhba64 (online) 67 | ``` 68 | 69 | ``` 70 | $ check_vsphere host-storage -u naemon@vsphere.local -s vcenter.example.com \ 71 | --vihost esx1.example.com --mode adapter --allowed vmhba64 72 | OK: Adapters 4; ignored: 3; online: 1 73 | iSCSI Software Adapter vmhba64 (online) 74 | ``` 75 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/media.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | checks if there are any vms on a host that have connected cd or floppy drives 21 | 22 | This is not good because vms cannot move hosts with mounted cds/floppies 23 | """ 24 | 25 | __cmd__ = 'media' 26 | 27 | from pyVmomi import vim 28 | from monplugin import Check, Status 29 | from .. import CheckVsphereException 30 | from ..tools import cli, service_instance 31 | from ..tools.helper import find_entity_views, isbanned, isallowed, CheckArgument 32 | 33 | def run(): 34 | parser = cli.Parser() 35 | # parser.add_optional_arguments(cli.Argument.DATACENTER_NAME) 36 | parser.add_optional_arguments(cli.Argument.VIHOST) 37 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex match against vm name')) 38 | parser.add_optional_arguments(CheckArgument.BANNED('regex match against vm name')) 39 | args = parser.get_args() 40 | si = service_instance.connect(args) 41 | 42 | check = Check() 43 | 44 | if args.vihost: 45 | host_view = si.content.viewManager.CreateContainerView( 46 | si.content.rootFolder, [vim.HostSystem], True) 47 | try: 48 | parentView = next(x for x in host_view.view if x.name.lower() == args.vihost.lower()) 49 | except: 50 | raise CheckVsphereException(f"host {args.vihost} not found") 51 | else: 52 | parentView = si.content.rootFolder 53 | 54 | #vm_view = si.content.viewManager.CreateContainerView(parentView, [vim.VirtualMachine], True) 55 | vms = find_entity_views( 56 | si, 57 | vim.VirtualMachine, 58 | begin_entity=parentView, 59 | properties=['name', 'config.hardware.device', 'config.template'] 60 | ) 61 | 62 | check.add_message( 63 | Status.OK, 64 | "no connected cdrom/floppy drives found" 65 | ) 66 | 67 | for vm in vms: 68 | match = 0 69 | if isbanned(args, vm['props']['name']): 70 | continue 71 | if not isallowed(args, vm['props']['name']): 72 | continue 73 | 74 | if vm['props']['runtime.powerState'] != 'poweredOn': 75 | # it's powered off, api is unreliable 76 | # config.hardware or config.template might be missing 77 | continue 78 | 79 | if vm['props']['config.template']: 80 | # This vm is a template, ignore it 81 | continue 82 | 83 | for device in vm['props']['config.hardware.device']: 84 | if \ 85 | (isinstance(device, vim.vm.device.VirtualCdrom) 86 | or isinstance(device, vim.vm.device.VirtualFloppy)) \ 87 | and device.connectable.connected: 88 | match += 1 89 | if match > 0: 90 | check.add_message( 91 | Status.CRITICAL, 92 | f'{vm["props"]["name"]} has cdrom/floppy drives connected' 93 | ) 94 | 95 | (code, message) = check.check_messages(separator=' - ') 96 | check.exit( 97 | code=code, 98 | message=message 99 | ) 100 | 101 | 102 | if __name__ == "__main__": 103 | run() 104 | -------------------------------------------------------------------------------- /stuff/vmware_tags_rest_api.py: -------------------------------------------------------------------------------- 1 | from requests import Session 2 | from typing import List, Dict 3 | from pprint import pprint as pp 4 | 5 | VALUE_KEY = 'value' 6 | 7 | 8 | class VMTagReader: 9 | def __init__(self, vcenter_url: str, login: str, password: str): 10 | self.host = vcenter_url 11 | self.session = Session() 12 | self.session.auth = (login, password) 13 | 14 | def read_vms_tags(self) -> Dict[str, Dict[str, List[str]]]: 15 | """ 16 | Read categories and theirs tags associated with VM 17 | 18 | :return: Dict{vm_id: Dict{category:tag_list}} 19 | """ 20 | 21 | self.__connect() 22 | cat_ids = self.__read_categories_ids() 23 | cat_infos = self.__read_categories_info(cat_ids) 24 | tag_assoc = self.__read_tags_assoc() 25 | return self.__read_tags_category(cat_infos, tag_assoc) 26 | 27 | def __connect(self) -> None: 28 | # curl -X POST https://my-company-vcenter.com/rest/com/vmware/cis/session -u my_login:my_pass 29 | res = self.session.post(f'{self.host}/rest/com/vmware/cis/session') 30 | res.raise_for_status() 31 | self.session.headers.update({'vmware-api-session-id': res.json()[VALUE_KEY]}) 32 | 33 | def __read_categories_ids(self) -> List[str]: 34 | # curl -X GET 'https://my-company-vcenter.com/rest/com/vmware/cis/tagging/category' -H "vmware-api-session-id:32e4cxxxxxxxx99a300a64312xxxx2f5" 35 | res = self.session.get(f'{self.host}/rest/com/vmware/cis/tagging/category') 36 | res.raise_for_status() 37 | print("CATEGORIES") 38 | pp(res.json()) 39 | return res.json()[VALUE_KEY] 40 | 41 | def __read_categories_info(self, cat_ids: List[str]) -> Dict[str, Dict]: 42 | # curl -X GET 'https://my-company-vcenter.com/rest/com/vmware/cis/tagging/category/id:urn:vmomi:InventoryServiceCategory:90xxxx9d-4xxd-4xx0-8xxx8-02804xxxx57f:GLOBAL' -H "vmware-api-session-id:{id}" 43 | cat_infos: Dict[str, Dict] = {} 44 | print("CATEGORY INFOS") 45 | for ci in cat_ids: 46 | res = self.session.get(f'{self.host}/rest/com/vmware/cis/tagging/category/id:{ci}') 47 | res.raise_for_status() 48 | cat_infos[ci] = res.json()[VALUE_KEY] 49 | pp(cat_infos) 50 | return cat_infos 51 | 52 | def __read_tags_assoc(self) -> Dict[str, List[str]]: 53 | # curl -X GET https://my-company-vcenter.com/api/vcenter/tagging/associations -H "vmware-api-session-id:{id}" 54 | res = self.session.get(f'{self.host}/api/vcenter/tagging/associations') 55 | res.raise_for_status() 56 | tag_assoc: Dict[str, List[str]] = {} 57 | print("TAG ASSOC") 58 | pp(res.json()) 59 | for a in res.json()['associations']: 60 | obj = a['object'] 61 | if obj['type'] == 'VirtualMachine': 62 | vm_id = obj['id'] 63 | if vm_id not in tag_assoc: 64 | tag_assoc[vm_id] = [] 65 | tag_assoc[vm_id].append(a['tag']) 66 | 67 | pp(tag_assoc) 68 | return tag_assoc 69 | 70 | def __read_tags_category(self, cat_infos: Dict[str, str], tag_assoc: Dict[str, List[str]]) -> Dict[str, Dict[str, List[str]]]: 71 | # curl -X GET 'https://my-company-vcenter.com/rest/com/vmware/cis/tagging/tag/id:urn:vmomi:InventoryServiceTag:01xx0e6b-fxxc-4xx5-a086-3220xxa1e917:GLOBAL' -H "vmware-api-session-id:{id}" 72 | vm_cat_tags = {} 73 | print("TAG CATEGORY") 74 | for vi, tags in tag_assoc.items(): 75 | for t in tags: 76 | res = self.session.get(f'{self.host}/rest/com/vmware/cis/tagging/tag/id:{t}') 77 | res.raise_for_status() 78 | pp((t, res.json())) 79 | ci = res.json()[VALUE_KEY]['category_id'] 80 | cat_name = cat_infos[ci]['name'] 81 | tag_name = res.json()[VALUE_KEY]['name'] 82 | if vi not in vm_cat_tags: 83 | vm_cat_tags[vi] = {} 84 | if cat_name not in vm_cat_tags[vi]: 85 | vm_cat_tags[vi][cat_name] = [] 86 | vm_cat_tags[vi][cat_name].append(tag_name) 87 | return vm_cat_tags 88 | 89 | 90 | if __name__ == "__main__": 91 | import sys, os 92 | vmr = VMTagReader( 93 | vcenter_url=sys.argv[1], 94 | login=sys.argv[2], 95 | password=( os.environ.get("VSPHERE_PASS", None) or sys.argv[3] )) 96 | vm_tags = vmr.read_vms_tags() 97 | print(vm_tags) 98 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/hostservice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | check services on host 20 | """ 21 | 22 | __cmd__ = 'host-service' 23 | 24 | from pyVmomi import vim, vmodl 25 | from monplugin import Check, Status 26 | from ..tools import cli, service_instance 27 | from ..tools.helper import ( 28 | CheckArgument, 29 | find_entity_views, 30 | isallowed, 31 | isbanned, 32 | process_retrieve_content 33 | ) 34 | 35 | def run(): 36 | global args 37 | parser = cli.Parser() 38 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of datastore')) 39 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of datastore')) 40 | parser.add_optional_arguments(cli.Argument.VIHOST) 41 | parser.add_optional_arguments({ 42 | 'name_or_flags': ['--maintenance-state'], 43 | 'options': { 44 | 'action': 'store', 45 | 'default': 'UNKNOWN', 46 | 'choices': ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'], 47 | 'help': 'exit with this status if the host is in maintenance, ' 48 | 'default UNKNOWN, or CRITICAL if --mode maintenance' 49 | } 50 | }) 51 | args = parser.get_args() 52 | 53 | si = service_instance.connect(args) 54 | check = Check() 55 | 56 | try: 57 | host = find_entity_views( 58 | si, 59 | vim.HostSystem, 60 | begin_entity=si.content.rootFolder, 61 | sieve=( {'name': args.vihost} if args.vihost else None ), 62 | properties=["name", "configManager", "runtime.inMaintenanceMode"], 63 | )[0] 64 | except IndexError: 65 | check.exit(Status.UNKNOWN, f"host {args.vihost or ''} not found") 66 | 67 | if host['props']['runtime.inMaintenanceMode']: 68 | check.exit( 69 | Status[args.maintenance_state], 70 | f"host {host['props']['name']} is in maintenance" 71 | ) 72 | 73 | count = { 74 | 'running': 0, 75 | 'not running': 0, 76 | } 77 | serviceSystem = service_system(si, host) 78 | for service in serviceSystem['serviceInfo'].service: 79 | serviceName = service.key 80 | serviceState = service.running 81 | 82 | if isbanned(args, serviceName): 83 | continue 84 | if not isallowed(args, serviceName): 85 | continue 86 | 87 | if not serviceState: 88 | check.add_message(Status.CRITICAL, f"{serviceName} not running") 89 | count['not running']+=1 90 | else: 91 | check.add_message(Status.OK, f"{serviceName} running") 92 | count['running']+=1 93 | 94 | (status, message) = check.check_messages(separator="\n", separator_all="\n") 95 | short = f"running: {count['running']}; not running: {count['not running']}" 96 | check.exit(status, f"{short}\n{message}") 97 | 98 | def service_system(si: vim.ServiceInstance, host): 99 | ObjectSpec = vmodl.query.PropertyCollector.ObjectSpec 100 | retrieve = si.content.propertyCollector.RetrieveContents 101 | propspec = vmodl.query.PropertyCollector.PropertySpec( 102 | all=False, 103 | pathSet=['serviceInfo'], 104 | type=vim.host.ServiceSystem 105 | ) 106 | 107 | objs = [ObjectSpec(obj=host['props']['configManager'].serviceSystem)] 108 | 109 | filter_spec = vmodl.query.PropertyCollector.FilterSpec( 110 | objectSet = objs, 111 | propSet = [propspec], 112 | ) 113 | 114 | result = retrieve( [filter_spec] ) 115 | service_system = process_retrieve_content(result) 116 | return service_system[0] 117 | 118 | if __name__ == "__main__": 119 | run() 120 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/hostnic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Check NICs on the host 20 | """ 21 | 22 | __cmd__ = 'host-nic' 23 | 24 | from pyVmomi import vim 25 | from monplugin import Check, Status 26 | from ..tools import cli, service_instance 27 | from ..tools.helper import find_entity_views, CheckArgument, isbanned 28 | 29 | def run(): 30 | parser = cli.Parser() 31 | # parser.add_optional_arguments(cli.Argument.DATACENTER_NAME) 32 | parser.add_optional_arguments(cli.Argument.VIHOST) 33 | parser.add_optional_arguments(CheckArgument.BANNED( 34 | 'regex, check against nic name' 35 | )) 36 | parser.add_optional_arguments( { 37 | 'name_or_flags': ['--maintenance-state'], 38 | 'options': { 39 | 'action': 'store', 40 | 'choices': ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'], 41 | 'default': 'UNKNOWN', 42 | 'help': 'exit with this status if the host is in maintenance, default UNKNOWN' 43 | } 44 | }) 45 | parser.add_optional_arguments({ 46 | 'name_or_flags': ['--unplugged-state'], 47 | 'options': { 48 | 'action': 'store', 49 | 'choices': ['OK', 'WARNING', 'CRITICAL'], 50 | 'default': 'WARNING', 51 | 'help': 'treat unplugged nics with that status code' 52 | } 53 | }) 54 | 55 | args = parser.get_args() 56 | 57 | si = service_instance.connect(args) 58 | check = Check() 59 | 60 | #vm_view = si.content.viewManager.CreateContainerView(parentView, [vim.VirtualMachine], True) 61 | try: 62 | vm = find_entity_views( 63 | si, 64 | vim.HostSystem, 65 | begin_entity=si.content.rootFolder, 66 | sieve=({'name': args.vihost} if args.vihost else None), 67 | properties=["name", "configManager.networkSystem", "runtime.inMaintenanceMode"] 68 | )[0] 69 | except IndexError: 70 | check.exit(Status.UNKNOWN, f"host {args.vihost or ''} not found") 71 | 72 | if vm['props']['runtime.inMaintenanceMode']: 73 | check.exit( 74 | Status[args.maintenance_state], 75 | f"host {vm['props']['name']} is in maintenance" 76 | ) 77 | 78 | network_system = vm["props"]["configManager.networkSystem"] 79 | network_info = network_system.networkInfo 80 | 81 | if not network_info: 82 | check.exit( 83 | Status.CRITICAL, 84 | f"{args.vihost} has no network info in the API" 85 | ) 86 | 87 | # physical nics 88 | pnics = {} 89 | switches = [] 90 | for pnic in network_info.pnic: 91 | pnics[pnic.key] = pnic 92 | 93 | if network_info.vswitch: 94 | switches.extend(network_info.vswitch) 95 | if network_info.proxySwitch: 96 | switches.extend(network_info.proxySwitch) 97 | 98 | for switch in switches: 99 | for nic in (switch.pnic or []): 100 | if isbanned(args, pnics[str(nic)].device): 101 | continue 102 | 103 | if not pnics[nic].linkSpeed: 104 | status = Status[args.unplugged_state] 105 | appendix="" 106 | if status == Status.OK: 107 | appendix = " , but ok due to `--unplugged_state OK`" 108 | check.add_message(status, f"{pnics[str(nic)].device} is unplugged{appendix}") 109 | else: 110 | check.add_message(Status.OK, f"{pnics[str(nic)].device} is ok") 111 | 112 | (code, message) = check.check_messages(separator="\n", separator_all='\n') 113 | check.exit( 114 | code=code, 115 | message=( message or "everything ok" ) 116 | ) 117 | 118 | if __name__ == "__main__": 119 | run() 120 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/vmnetdev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | check network devices of vms 21 | """ 22 | 23 | __cmd__ = "vm-net-dev" 24 | 25 | import logging 26 | from pyVmomi import vim 27 | from monplugin import Check, Status 28 | from ..tools import cli, service_instance 29 | from ..tools.helper import ( 30 | CheckArgument, 31 | find_entity_views, 32 | isbanned, 33 | isallowed, 34 | ) 35 | 36 | check = None 37 | args = None 38 | 39 | 40 | def get_argparser(): 41 | parser = cli.Parser() 42 | parser.add_required_arguments( 43 | { 44 | "name_or_flags": ["--mode"], 45 | "options": { 46 | "action": "store", 47 | "choices": ["start-unconnected"], 48 | "help": "check for vms that have network cards configured " 49 | "that are not connected on startup." 50 | '--exclude and --include match against a string like "vmname;Network adapter 1"', 51 | }, 52 | } 53 | ) 54 | 55 | parser.add_optional_arguments(CheckArgument.BANNED("regex")) 56 | parser.add_optional_arguments(CheckArgument.ALLOWED("regex")) 57 | 58 | return parser 59 | 60 | 61 | def check_start_not_connected(vm): 62 | vmname = vm["props"]["name"] 63 | 64 | for d in vm["props"]["config.hardware"].device: 65 | if "VirtualEthernetCard" in str(type(d.backing)): 66 | probe = f"{vmname};{d.deviceInfo.label}" 67 | 68 | if isbanned(args, probe): 69 | logging.debug(("banned", probe)) 70 | continue 71 | 72 | if not isallowed(args, probe): 73 | logging.debug(("not allowed", probe)) 74 | continue 75 | 76 | if not d.connectable.connected: 77 | logging.debug(("running and disconnected", probe)) 78 | continue 79 | 80 | if not d.connectable.startConnected: 81 | check.add_message( 82 | Status.CRITICAL, f"Connect At Power On is off for {probe}" 83 | ) 84 | 85 | 86 | def run(): 87 | global check 88 | global args 89 | 90 | parser = get_argparser() 91 | args = parser.get_args() 92 | 93 | check = Check() 94 | 95 | args._si = service_instance.connect(args) 96 | 97 | vms = find_entity_views( 98 | args._si, 99 | vim.VirtualMachine, 100 | begin_entity=args._si.content.rootFolder, 101 | properties=["name", "runtime.powerState", "config.template", "config.hardware"], 102 | ) 103 | 104 | for vm in vms: 105 | name = vm["props"]["name"] 106 | isTemplate = vm["props"].get("config.template", None) 107 | powered = vm["props"].get("runtime.powerState", None) == "poweredOn" 108 | 109 | if not powered: 110 | logging.debug(f"{name} is powered off") 111 | continue 112 | 113 | if isTemplate: 114 | logging.debug(f"{name} is a template vm, ignoring ...") 115 | continue 116 | 117 | if args.mode == "start-unconnected": 118 | check_start_not_connected(vm) 119 | 120 | (code, message) = check.check_messages(separator="\n", separator_all="\n") 121 | check.exit( 122 | code=code, 123 | message="all checks ok" if code == Status.OK else message, 124 | ) 125 | 126 | 127 | if __name__ == "__main__": 128 | try: 129 | run() 130 | except SystemExit as e: 131 | if e.code > 3 or e.code < 0: 132 | print("UNKNOWN EXIT CODE") 133 | raise SystemExit(Status.UNKNOWN) 134 | except Exception as e: 135 | print("UNKNOWN - " + str(e)) 136 | raise SystemExit(Status.UNKNOWN) 137 | -------------------------------------------------------------------------------- /stuff/get_entity_view.pl: -------------------------------------------------------------------------------- 1 | sub find_entity_view { 2 | my $self = &_select_vim; 3 | my %args = @_; 4 | my $service = $self->{vim_service}; 5 | my $sc = $self->{service_content}; 6 | 7 | if (! exists($args{view_type})) { 8 | Carp::confess('view_type argument is required'); 9 | } 10 | my $view_type = $args{view_type}; 11 | 12 | eval { 13 | VIMRuntime::load($view_type); 14 | }; 15 | if ($@) { 16 | Carp::croak "Unable to load class '$view_type' for find_entity_view(): Perhaps it is not a valid managed entity type"; 17 | } 18 | 19 | delete $args{view_type}; 20 | if (! $view_type->isa('EntityViewBase')) { 21 | Carp::confess("$view_type is not a ManagedEntity"); 22 | } 23 | my $properties = ""; 24 | if (exists ($args{properties})) { 25 | if (defined($args{properties})) { 26 | $properties = $args{properties}; 27 | } 28 | delete $args{properties}; 29 | } 30 | my $begin_entity = $sc->rootFolder; 31 | if (exists ($args{begin_entity})) { 32 | $begin_entity = $args{begin_entity}; 33 | delete $args{begin_entity}; 34 | } 35 | my $filter = {}; 36 | if (exists ($args{filter})) { 37 | $filter = $args{filter}; 38 | delete $args{filter}; 39 | } 40 | 41 | my @remaining = keys %args; 42 | if (@remaining > 0) { 43 | Carp::confess("Unexpected argument @remaining"); 44 | } 45 | my %filter_hash; 46 | foreach (keys %$filter) { 47 | my $key = $_; 48 | my $keyvalue = $filter->{$_}; 49 | my $index = index($_, "-"); 50 | if($index == 0) { 51 | $key = substr($key,1); 52 | } 53 | $filter_hash{$key} = $keyvalue; 54 | } 55 | my $property_spec = PropertySpec->new(all => 0, 56 | type => $view_type->get_backing_type(), 57 | pathSet => [keys %filter_hash]); 58 | 59 | my $service_url = $self->{service_url}; 60 | my %result = query_api_supported($service_url); 61 | my $is_exists_vimversion_xml = $result{"supported"}; 62 | my $property_filter_spec; 63 | if($is_exists_vimversion_xml eq '1') { 64 | $property_filter_spec = 65 | $view_type->get_search_filter_spec_v40($begin_entity, [$property_spec], %result); 66 | } 67 | else { 68 | $property_filter_spec = 69 | $view_type->get_search_filter_spec($begin_entity, [$property_spec]); 70 | } 71 | 72 | my $obj_contents = 73 | $service->RetrieveProperties(_this => $sc->propertyCollector, 74 | specSet => $property_filter_spec); 75 | my $result = Util::check_fault($obj_contents); 76 | my $filtered_obj; 77 | foreach (@$result) { 78 | my $obj_content = $_; 79 | if (keys %$filter == 0) { 80 | $filtered_obj = $obj_content->obj; 81 | last; 82 | } 83 | 84 | my %prop_hash; 85 | my $prop_set = $obj_content->propSet; 86 | if (! $prop_set) { 87 | next; 88 | } 89 | foreach (@$prop_set) { 90 | $prop_hash{$_->name} = $_->val; 91 | } 92 | my $matched = 1; 93 | foreach (keys %$filter) { 94 | my $index = index($_, "-"); 95 | my $regex = $filter->{$_}; 96 | my $flag = 0; 97 | if($index == 0) { 98 | $_ = substr($_,1); 99 | $flag=1; 100 | } 101 | if (exists ($prop_hash{$_})) { 102 | my $val; 103 | if (ref $prop_hash{$_}) { 104 | my $class = ref $prop_hash{$_}; 105 | if ($class->isa("SimpleType")) { 106 | $val = $prop_hash{$_}->val(); 107 | } else { 108 | Carp::croak("Filtering is only supported for Simple Types\n"); 109 | } 110 | } else { 111 | $val = $prop_hash{$_}; 112 | } 113 | 114 | if($flag == 0) { 115 | if (not match($regex,$val)) { 116 | $matched = 0; 117 | last; 118 | } 119 | } 120 | else { 121 | if (not mismatch($regex,$val)) { 122 | $matched = 0; 123 | last; 124 | } 125 | } 126 | } 127 | } 128 | if ($matched) { 129 | $filtered_obj = $obj_content->obj; 130 | } 131 | } 132 | if (! $filtered_obj) { 133 | return undef; 134 | } else { 135 | return $self->get_view(mo_ref => $filtered_obj, 136 | view_type => $view_type, 137 | properties => $properties); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /stuff/get_entity_views.pl: -------------------------------------------------------------------------------- 1 | sub find_entity_views { 2 | my $self = &_select_vim; 3 | my %args = @_; 4 | my $service = $self->{vim_service}; 5 | my $sc = $self->{service_content}; 6 | 7 | if (! exists($args{view_type})) { 8 | Carp::confess('view_type argument is required'); 9 | } 10 | my $view_type = $args{view_type}; 11 | 12 | eval { 13 | VIMRuntime::load($view_type); 14 | }; 15 | if ($@) { 16 | Carp::croak "Unable to load class '$view_type' for find_entity_views(): Perhaps it is not a valid managed entity type"; 17 | } 18 | 19 | delete $args{view_type}; 20 | if (! $view_type->isa('EntityViewBase')) { 21 | Carp::confess("$view_type is not a ManagedEntity"); 22 | } 23 | my $begin_entity = $sc->rootFolder; 24 | if (exists ($args{begin_entity})) { 25 | $begin_entity = $args{begin_entity}; 26 | delete $args{begin_entity}; 27 | } 28 | my $filter = {}; 29 | if (exists ($args{filter})) { 30 | $filter = $args{filter}; 31 | delete $args{filter}; 32 | } 33 | my $properties = ""; 34 | if (exists ($args{properties})) { 35 | if (defined($args{properties})) { 36 | $properties = $args{properties}; 37 | } 38 | delete $args{properties}; 39 | } 40 | my @remaining = keys %args; 41 | if (@remaining > 0) { 42 | Carp::confess("Unexpected argument @remaining"); 43 | } 44 | 45 | my %filter_hash; 46 | foreach (keys %$filter) { 47 | my $key = $_; 48 | my $keyvalue = $filter->{$_}; 49 | my $index = index($_, "-"); 50 | if($index == 0) { 51 | $key = substr($key,1); 52 | } 53 | $filter_hash{$key} = $keyvalue; 54 | } 55 | 56 | my $property_spec = PropertySpec->new(all => 0, 57 | type => $view_type->get_backing_type(), 58 | pathSet => [keys %filter_hash]); 59 | 60 | my $service_url = $self->{service_url}; 61 | my %result = query_api_supported($service_url); 62 | my $is_exists_vimversion_xml = $result{"supported"}; 63 | my $property_filter_spec; 64 | if($is_exists_vimversion_xml eq '1') { 65 | $property_filter_spec = 66 | $view_type->get_search_filter_spec_v40($begin_entity, [$property_spec], %result); 67 | } 68 | else { 69 | $property_filter_spec = 70 | $view_type->get_search_filter_spec($begin_entity, [$property_spec]); 71 | } 72 | 73 | my $obj_contents = 74 | $service->RetrieveProperties(_this => $sc->propertyCollector, 75 | specSet => $property_filter_spec); 76 | my $result = Util::check_fault($obj_contents); 77 | my @filtered_objs; 78 | foreach (@$result) { 79 | my $obj_content = $_; 80 | if (keys %$filter == 0) { 81 | push @filtered_objs, $obj_content->obj; 82 | next; 83 | } 84 | 85 | my %prop_hash; 86 | my $prop_set = $obj_content->propSet; 87 | if (! $prop_set) { 88 | nextub 89 | } 90 | foreach (@$prop_set) { 91 | $prop_hash{$_->name} = $_->val; 92 | } 93 | my $matched = 1; 94 | foreach (keys %$filter) { 95 | my $index = index($_, "-"); 96 | my $regex = $filter->{$_}; 97 | my $flag = 0; 98 | if($index == 0) { 99 | $_ = substr($_,1); 100 | $flag=1; 101 | } 102 | if (exists ($prop_hash{$_})) { 103 | my $val; 104 | if (ref $prop_hash{$_}) { 105 | my $class = ref $prop_hash{$_}; 106 | if ($class->isa("SimpleType")) { 107 | $val = $prop_hash{$_}->val(); 108 | } else { 109 | Carp::croak("Filtering is only supported for Simple Type\n"); 110 | } 111 | } else { 112 | $val = $prop_hash{$_}; 113 | } 114 | 115 | if($flag == 0) { 116 | if (not match($regex,$val)) { 117 | $matched = 0; 118 | last; 119 | } 120 | } 121 | else { 122 | if (not mismatch($regex,$val)) { 123 | $matched = 0; 124 | last; 125 | } 126 | } 127 | } 128 | } 129 | if ($matched) { 130 | push @filtered_objs, $obj_content->obj; 131 | } 132 | } 133 | if (! @filtered_objs) { 134 | return []; 135 | } else { 136 | return $self->get_views(mo_ref_array => \@filtered_objs, 137 | view_type => $view_type, 138 | properties => $properties); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /checkvsphere/tools/serviceutil.py: -------------------------------------------------------------------------------- 1 | # VMware vSphere Python SDK 2 | # Copyright (c) 2008-2021 VMware, Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # LICENSE-VMWARE 17 | 18 | """ 19 | Utility functions for the vSphere API 20 | 21 | See com.vmware.apputils.vim25.ServiceUtil in the java API. 22 | """ 23 | 24 | from pyVmomi import vim, vmodl 25 | 26 | 27 | def build_full_traversal(): 28 | """ 29 | Builds a traversal spec that will recurse through all objects .. or at 30 | least I think it does. additions welcome. 31 | 32 | See com.vmware.apputils.vim25.ServiceUtil.buildFullTraversal in the java 33 | API. Extended by Sebastian Tello's examples from pysphere to reach networks 34 | and datastores. 35 | """ 36 | 37 | traversal_spec = vmodl.query.PropertyCollector.TraversalSpec 38 | selection_spec = vmodl.query.PropertyCollector.SelectionSpec 39 | 40 | # Recurse through all resourcepools 41 | rp_to_rp = traversal_spec(name='rpToRp', type=vim.ResourcePool, path="resourcePool", skip=False) 42 | 43 | rp_to_rp.selectSet.extend( 44 | ( 45 | selection_spec(name="rpToRp"), 46 | selection_spec(name="rpToVm"), 47 | ) 48 | ) 49 | 50 | rp_to_vm = traversal_spec(name='rpToVm', type=vim.ResourcePool, path="vm", skip=False) 51 | 52 | # Traversal through resourcepool branch 53 | cr_to_rp = traversal_spec( 54 | name='crToRp', type=vim.ComputeResource, path='resourcePool', skip=False) 55 | cr_to_rp.selectSet.extend( 56 | ( 57 | selection_spec(name='rpToRp'), 58 | selection_spec(name='rpToVm'), 59 | ) 60 | ) 61 | 62 | # Traversal through host branch 63 | cr_to_h = traversal_spec(name='crToH', type=vim.ComputeResource, path='host', skip=False) 64 | 65 | # Traversal through hostFolder branch 66 | dc_to_hf = traversal_spec(name='dcToHf', type=vim.Datacenter, path='hostFolder', skip=False) 67 | dc_to_hf.selectSet.extend( 68 | ( 69 | selection_spec(name='visitFolders'), 70 | ) 71 | ) 72 | 73 | # Traversal through vmFolder branch 74 | dc_to_vmf = traversal_spec(name='dcToVmf', type=vim.Datacenter, path='vmFolder', skip=False) 75 | dc_to_vmf.selectSet.extend( 76 | ( 77 | selection_spec(name='visitFolders'), 78 | ) 79 | ) 80 | 81 | # Traversal through network folder branch 82 | dc_to_net = traversal_spec( 83 | name='dcToNet', type=vim.Datacenter, path='networkFolder', skip=False) 84 | dc_to_net.selectSet.extend( 85 | ( 86 | selection_spec(name='visitFolders'), 87 | ) 88 | ) 89 | 90 | # Traversal through datastore branch 91 | dc_to_ds = traversal_spec(name='dcToDs', type=vim.Datacenter, path='datastore', skip=False) 92 | dc_to_ds.selectSet.extend( 93 | ( 94 | selection_spec(name='visitFolders'), 95 | ) 96 | ) 97 | 98 | # Recurse through all hosts 99 | h_to_vm = traversal_spec(name='hToVm', type=vim.HostSystem, path='vm', skip=False) 100 | h_to_vm.selectSet.extend( 101 | ( 102 | selection_spec(name='visitFolders'), 103 | ) 104 | ) 105 | 106 | # Recurse through the folders 107 | visit_folders = traversal_spec( 108 | name='visitFolders', type=vim.Folder, path='childEntity', skip=False) 109 | visit_folders.selectSet.extend( 110 | ( 111 | selection_spec(name='visitFolders'), 112 | selection_spec(name='dcToHf'), 113 | selection_spec(name='dcToVmf'), 114 | selection_spec(name='dcToNet'), 115 | selection_spec(name='crToH'), 116 | selection_spec(name='crToRp'), 117 | selection_spec(name='dcToDs'), 118 | selection_spec(name='hToVm'), 119 | selection_spec(name='rpToVm'), 120 | ) 121 | ) 122 | 123 | full_traversal = selection_spec.Array( 124 | (visit_folders, dc_to_hf, dc_to_vmf, dc_to_net, cr_to_h, cr_to_rp, dc_to_ds, rp_to_rp, 125 | h_to_vm, rp_to_vm,)) 126 | 127 | return full_traversal 128 | 129 | 130 | # vim: set ts=4 sw=4 expandtab filetype=python: 131 | -------------------------------------------------------------------------------- /checkvsphere/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import os 19 | import signal 20 | import sys 21 | import pkgutil 22 | import importlib 23 | import checkvsphere.vcmd 24 | from checkvsphere import VsphereConnectException, CheckVsphereTimeout 25 | from pyVmomi import vim 26 | 27 | def timeout_handler(signum, frame): 28 | raise CheckVsphereTimeout("Timeout reached") 29 | 30 | def set_timeout(seconds=None, handler=None): 31 | if seconds is None: 32 | seconds = int(os.environ.get("TIMEOUT", "30")) 33 | signal.signal(signal.SIGALRM, (handler or timeout_handler)) 34 | signal.alarm(seconds) 35 | 36 | def run(): 37 | cmd = None 38 | try: 39 | cmd = sys.argv.pop(1) 40 | except: 41 | pass 42 | 43 | set_timeout() 44 | 45 | if cmd and cmd not in ['-h', 'help', '--help']: 46 | mod = "".join(c for c in cmd if c.isalnum()) 47 | try: 48 | runner = importlib.import_module(f"checkvsphere.vcmd.{mod}") 49 | except ModuleNotFoundError as e: 50 | if not e.name.startswith("checkvsphere.vcmd."): 51 | raise e 52 | print(f"command not found: {cmd}") 53 | sys.exit(3) 54 | try: 55 | sys.argv[0] = f"{sys.argv[0]} {runner.__cmd__}" 56 | except: 57 | sys.argv[0] = f"{sys.argv[0]} {cmd}" 58 | runner.run() 59 | else: 60 | p ={} 61 | cmds = set() 62 | for loader, name, is_pkg in pkgutil.walk_packages(checkvsphere.vcmd.__path__): 63 | if not is_pkg: 64 | full_name = checkvsphere.vcmd.__name__ + '.' + name 65 | p[name] = importlib.import_module(full_name) 66 | if hasattr(p[name], '__cmd__') and p[name].__cmd__: 67 | cmds.add(p[name].__cmd__) 68 | print("Specify cmd, one of:\n") 69 | for cmd in sorted(cmds): 70 | print(f" {cmd}") 71 | print() 72 | 73 | 74 | def main(): 75 | import traceback 76 | import logging 77 | 78 | if int(os.environ.get("VSPHERE_DEBUG", "0")) > 0: 79 | logging.basicConfig( 80 | level=logging.DEBUG, 81 | format='%(asctime)s %(levelname)s %(message)s', 82 | stream=sys.stderr 83 | ) 84 | 85 | try: 86 | run() 87 | except VsphereConnectException as e: 88 | print("Cannot connect - ", end="") 89 | if e.__cause__ and hasattr(e.__cause__, "msg"): 90 | print(e.__cause__.msg) 91 | elif e.__cause__ and hasattr(e.__cause__, "message"): 92 | print(e.__cause__.message) 93 | elif e.__cause__: 94 | print(str(e.__cause__)) 95 | else: 96 | print(str(e)) 97 | sys.exit(0) 98 | except SystemExit as e: 99 | if not isinstance(e.code, int) or e.code > 3: 100 | sys.exit(3) 101 | else: 102 | sys.exit(e.code) 103 | except CheckVsphereTimeout: 104 | print("UNKNOWN - Timeout reached") 105 | if int(os.environ.get("VSPHERE_DEBUG", "0")) > 0: 106 | traceback.print_exc(file=sys.stdout) 107 | sys.exit(3) 108 | except ConnectionRefusedError: 109 | print("UNKNOWN - Connection refused") 110 | raise SystemExit(2) 111 | except vim.fault.VimFault as e: 112 | if hasattr(e, 'msg'): 113 | print(f"ERROR - {e.msg}") 114 | else: 115 | # in case there is no msg attribute 116 | # According to the docs there is 117 | # faultCause and faultMessage, but they are empty 118 | # but there is a msg attribute (which is not in the docs) 119 | # i don't know if it is set always 120 | # so fall back to the normal string representation 121 | print(f"ERROR - {e}") 122 | if int(os.environ.get("VSPHERE_DEBUG", "0")) > 0: 123 | traceback.print_exc(file=sys.stdout) 124 | raise SystemExit(3) 125 | except Exception as e: 126 | print(f"UNKNOWN - Unhandled exception: {e}") 127 | if int(os.environ.get("VSPHERE_DEBUG", "0")) > 0: 128 | traceback.print_exc(file=sys.stdout) 129 | raise SystemExit(3) 130 | 131 | if __name__ == "__main__": 132 | main() 133 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/vmguestfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | check virtualmachine filesystems 20 | """ 21 | 22 | __cmd__ = "vm-guestfs" 23 | 24 | from pyVmomi import vim 25 | from monplugin import Check, Status, Threshold, Range 26 | from checkvsphere.tools import cli, service_instance 27 | from checkvsphere.tools.helper import ( 28 | CheckArgument, 29 | find_entity_views, 30 | isallowed, 31 | isbanned, 32 | ) 33 | from checkvsphere.vcmd.datastores import Space, range_in_bytes 34 | 35 | args = None 36 | 37 | 38 | def run(): 39 | global args 40 | parser = cli.Parser() 41 | parser.add_required_arguments(cli.Argument.VM_NAME) 42 | parser.add_optional_arguments(CheckArgument.BANNED("regex, of mountpoint")) 43 | parser.add_optional_arguments(CheckArgument.ALLOWED("regex, of mountpoint")) 44 | parser.add_optional_arguments(CheckArgument.CRITICAL_THRESHOLD) 45 | parser.add_optional_arguments(CheckArgument.WARNING_THRESHOLD) 46 | parser.add_optional_arguments( 47 | { 48 | "name_or_flags": ["--metric"], 49 | "options": { 50 | "action": "store", 51 | "default": "usage", 52 | "help": "The metric to apply the thresholds on, defaults to `usage`, can be: " 53 | "usage (in percent), free and used. " 54 | "free and used are measured in bytes. You can use one of these suffixes: " 55 | "kB, MB, GB for example: free_MB or used_GB", 56 | }, 57 | } 58 | ) 59 | args = parser.get_args() 60 | 61 | si = service_instance.connect(args) 62 | check = Check(threshold=Threshold(args.warning or None, args.critical or None)) 63 | 64 | try: 65 | vm = find_entity_views( 66 | si, 67 | vim.VirtualMachine, 68 | begin_entity=si.content.rootFolder, 69 | sieve={"name": args.vm_name}, 70 | properties=["name", "guest"], 71 | )[0] 72 | except IndexError: 73 | check.exit(Status.UNKNOWN, f"vm {args.vm_name} not found") 74 | 75 | # print(vm['props']['guest']) 76 | fs_info(check, vm["props"]["guest"].disk) 77 | 78 | 79 | def fs_info(check: Check, disks): 80 | filtered = False 81 | disk_count = len(disks) 82 | 83 | for disk in disks: 84 | name = disk.diskPath 85 | if isbanned(args, f"{name}"): 86 | disk_count -= 1 87 | continue 88 | if not isallowed(args, f"{name}"): 89 | disk_count -= 1 90 | filtered = True 91 | continue 92 | 93 | try: 94 | space = Space(disk.capacity, disk.freeSpace) 95 | except ZeroDivisionError: 96 | check.add_message(Status.CRITICAL, f"{name} has a capacity of zero") 97 | continue 98 | 99 | for metric in ["usage", "free", "used", "capacity"]: 100 | opts = {} 101 | 102 | # Check threshold against this metric 103 | if args.metric.startswith(metric) and (args.warning or args.critical): 104 | value = space[args.metric] 105 | _, uom, *_ = args.metric.split("_") + ["%" if "usage" in args.metric else "B"] 106 | s = check.threshold.get_status(space[args.metric]) 107 | 108 | threshold = {} 109 | opts["threshold"] = {} 110 | if args.warning: 111 | threshold["warning"] = range_in_bytes(Range(args.warning), uom) 112 | if args.critical: 113 | threshold["critical"] = range_in_bytes(Range(args.critical), uom) 114 | opts["threshold"] = Threshold(**threshold) 115 | 116 | if s != Status.OK: 117 | check.add_message(s, f"{args.metric} on {name} is in state {s.name}: {value :.2f}{uom}") 118 | 119 | puom = "%" if metric == "usage" else "B" 120 | check.add_perfdata(label=f"{name} {metric}", value=space[metric], uom=puom, **opts) 121 | 122 | if filtered and not disk_count: 123 | check.add_message(Status.WARNING, "no filesystems found") 124 | 125 | (code, message) = check.check_messages(separator="\n") # , allok=okmessage) 126 | check.exit(code=code, message=(message or "everything ok")) 127 | 128 | 129 | if __name__ == "__main__": 130 | run() 131 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/powerstate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | check power state of all hosts 21 | """ 22 | 23 | __cmd__ = 'power-state' 24 | 25 | from pyVmomi import vim 26 | from monplugin import Check, Status 27 | from .. import CheckVsphereException 28 | from ..tools import cli, service_instance 29 | from ..tools.helper import ( 30 | CheckArgument, 31 | find_entity_views, 32 | isbanned, 33 | isallowed, 34 | process_retrieve_content 35 | ) 36 | 37 | args = None 38 | 39 | def get_argparser(): 40 | parser = cli.Parser() 41 | parser.add_optional_arguments(CheckArgument.BANNED( 42 | 'regex, checked against ', 43 | )) 44 | parser.add_optional_arguments(CheckArgument.ALLOWED( 45 | 'regex, checked against ', 46 | )) 47 | parser.add_optional_arguments( CheckArgument.CRITICAL_THRESHOLD ) 48 | parser.add_optional_arguments( CheckArgument.WARNING_THRESHOLD ) 49 | parser.add_optional_arguments( cli.Argument.CLUSTER_NAME ) 50 | parser.add_optional_arguments({ 51 | 'name_or_flags': ['--metric'], 52 | 'options': { 53 | 'action': 'store', 54 | 'choices': ['total', 'up', 'down', 'ignored', 'up%', 'down%'], 55 | 'default': 'down', 56 | 'help': 'metric to apply thresholds on, defaults to "down"', 57 | } 58 | }) 59 | return parser 60 | 61 | def run(): 62 | global args 63 | parser = get_argparser() 64 | args = parser.get_args() 65 | 66 | check = Check() 67 | check.set_threshold(warning=args.warning, critical=args.critical) 68 | 69 | args._si = service_instance.connect(args) 70 | 71 | begin_entity=args._si.content.rootFolder 72 | if args.cluster_name: 73 | cluster = find_entity_views( 74 | args._si, 75 | vim.ClusterComputeResource, 76 | properties=['name'], 77 | sieve={'name': args.cluster_name}, 78 | ) 79 | try: 80 | begin_entity = cluster[0]['obj'].obj 81 | except: 82 | raise CheckVsphereException(f"cluster {args.cluster_name} not found") 83 | 84 | hosts = find_entity_views( 85 | args._si, 86 | vim.HostSystem, 87 | begin_entity=begin_entity, 88 | properties=['name', 'runtime.powerState'] 89 | ) 90 | hosts = process_retrieve_content(list(map(lambda x: x['obj'], hosts))) 91 | 92 | total = 0 93 | ignored = 0 94 | powered = 0 95 | unpowered = 0 96 | 97 | for host in hosts: 98 | total += 1 99 | if isbanned(args, f'{host["name"]}'): 100 | ignored += 1 101 | continue 102 | if not isallowed(args, f'{host["name"]}'): 103 | ignored += 1 104 | continue 105 | 106 | message = f"powerState of { host['name'] } is { host['runtime.powerState']}" 107 | 108 | if host['runtime.powerState'] in ['poweredOn']: # , 'MaintenanceMode']: 109 | powered += 1 110 | check.add_message(Status.OK, message) 111 | else: 112 | unpowered += 1 113 | check.add_message(Status.CRITICAL, message) 114 | 115 | metrics = { 116 | 'total': total, 117 | 'up': powered, 118 | 'down': unpowered, 119 | 'ignored': ignored, 120 | 'up%': 100*powered/(total - ignored) if total - ignored != 0 else -1, 121 | 'down%': 100*unpowered/(total - ignored) if total - ignored != 0 else -1, 122 | } 123 | 124 | for l, v in metrics.items(): 125 | opt = {} 126 | if l == args.metric and (args.warning or args.critical): 127 | opt['threshold'] = check.threshold 128 | 129 | check.add_perfdata(label=l, value=v, **opt) 130 | 131 | opt = {} 132 | if not args.verbose: 133 | opt['allok'] = 'All hosts ok' 134 | 135 | if args.warning or args.critical: 136 | code = check.check_threshold(metrics[args.metric]) 137 | check.exit(code=code, message=f"{ total } hosts, { powered } powered, {ignored} ignored, unpowered { unpowered }") 138 | else: 139 | (code, message) = check.check_messages(separator='\n', separator_all='\n', **opt) 140 | check.exit( 141 | code=code, 142 | message=f"{ total } hosts, { powered } powered, {ignored} ignored, unpowered { unpowered }\n" + message, 143 | ) 144 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/snapshots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | check age or number of vm snapshots 21 | """ 22 | 23 | __cmd__ = 'snapshots' 24 | 25 | import logging 26 | from pyVmomi import vim 27 | from monplugin import Check, Status 28 | from ..tools import cli, service_instance 29 | from datetime import datetime, timedelta, timezone 30 | from ..tools.helper import ( 31 | CheckArgument, 32 | find_entity_views, 33 | isbanned, 34 | isallowed 35 | ) 36 | 37 | check = None 38 | args = None 39 | 40 | def get_argparser(): 41 | parser = cli.Parser() 42 | parser.add_optional_arguments( CheckArgument.CRITICAL_THRESHOLD ) 43 | parser.add_optional_arguments( CheckArgument.WARNING_THRESHOLD ) 44 | parser.add_required_arguments( { 45 | 'name_or_flags': ['--mode'], 46 | 'options': { 47 | 'action': 'store', 48 | 'choices': ['age', 'count'], 49 | 'help': 'check thresholds against age/count of snapshots' 50 | } 51 | }) 52 | 53 | parser.add_optional_arguments(CheckArgument.BANNED( 54 | 'regex, checked against ;', 55 | )) 56 | parser.add_optional_arguments(CheckArgument.ALLOWED( 57 | 'regex, checked against ;', 58 | )) 59 | 60 | return parser 61 | 62 | def count_snapshots(vm, snaplist): 63 | count = 0 64 | vmname = vm['props']['name'] 65 | 66 | for snap in snaplist: 67 | 68 | if snap.childSnapshotList: 69 | count+=count_snapshots(vm, snap.childSnapshotList) 70 | 71 | snapname = snap.name 72 | 73 | if isbanned(args, f'{vmname};{snapname}'): 74 | logging.debug(('banned', f'{vmname};{snapname}')) 75 | continue 76 | if not isallowed(args, f'{vmname};{snapname}'): 77 | logging.debug(('not allowed', f'{vmname};{snapname}')) 78 | continue 79 | 80 | count+=1 81 | 82 | return count 83 | 84 | def check_by_age(vm, snaplist): 85 | vmname = vm['props']['name'] 86 | for snap in snaplist: 87 | if snap.childSnapshotList: 88 | check_by_age(vm, snap.childSnapshotList) 89 | 90 | snapname = snap.name 91 | 92 | if isbanned(args, f'{vmname};{snapname}'): 93 | logging.debug(('banned', f'{vmname};{snapname}')) 94 | continue 95 | if not isallowed(args, f'{vmname};{snapname}'): 96 | logging.debug(('not allowed', f'{vmname};{snapname}')) 97 | continue 98 | 99 | now = datetime.now(timezone.utc) 100 | age = (now - snap.createTime) / timedelta(days=1) 101 | code = check.check_threshold(age) 102 | if code != Status.OK: 103 | check.add_message(code, f"«{snapname}» on «{vmname}» is {age:.2f} days old") 104 | #print((code, f"«{snapname}» on «{vmname}» is {age:.2f} days old")) 105 | 106 | def run(): 107 | global check 108 | global args 109 | parser = get_argparser() 110 | args = parser.get_args() 111 | 112 | if not (args.warning or args.critical): 113 | raise Exception("at least one of --warning or --critical is required") 114 | 115 | check = Check() 116 | check.set_threshold(warning=args.warning, critical=args.critical) 117 | 118 | args._si = service_instance.connect(args) 119 | 120 | vms = find_entity_views( 121 | args._si, 122 | vim.VirtualMachine, 123 | begin_entity=args._si.content.rootFolder, 124 | properties=['name', 'snapshot', 'resourcePool', 'config.template'] 125 | ) 126 | 127 | for vm in vms: 128 | name = vm['props']['name'] 129 | isTemplate = vm['props'].get('config.template', None) 130 | 131 | if 'snapshot' not in vm['props']: 132 | logging.debug(f"vm {name} has no snapshots") 133 | continue 134 | 135 | if isTemplate: 136 | logging.debug(f"{name} is a template vm, ignoring ...") 137 | continue 138 | 139 | adj = None 140 | if args.mode == 'age': 141 | adj = 'old' 142 | check_by_age(vm, vm['props']['snapshot'].rootSnapshotList) 143 | elif args.mode == 'count': 144 | adj = 'many' 145 | count = count_snapshots(vm, vm['props']['snapshot'].rootSnapshotList) 146 | code = check.check_threshold(count) 147 | if code != Status.OK: 148 | check.add_message(code, f"«{name}» has {count} snapshots") 149 | else: 150 | raise RuntimeError("Unknown mode {args.mode}") 151 | 152 | (code, message) = check.check_messages(separator='\n', separator_all='\n') 153 | check.exit( 154 | code=code, 155 | message="snapshots ok" if code == Status.OK else f"too {adj} snapshots found\n" + message, 156 | ) 157 | 158 | if __name__ == "__main__": 159 | try: 160 | run() 161 | except SystemExit as e: 162 | if e.code > 3 or e.code < 0: 163 | print("UNKNOWN EXIT CODE") 164 | raise SystemExit(Status.UNKNOWN) 165 | except Exception as e: 166 | print("UNKNOWN - " + str(e)) 167 | raise SystemExit(Status.UNKNOWN) 168 | -------------------------------------------------------------------------------- /checkvsphere/tools/pchelper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Property Collector helper module. 3 | """ 4 | 5 | # VMware vSphere Python SDK Community Samples Addons 6 | # Copyright (c) 2014-2021 VMware, Inc. All Rights Reserved. 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | # See LICENSE-VMWARE 21 | 22 | 23 | import pyVmomi 24 | 25 | 26 | # Shamelessly borrowed from: 27 | # https://github.com/dnaeon/py-vconnector/blob/master/src/vconnector/core.py 28 | def collect_properties(si, view_ref, obj_type, path_set=None, 29 | include_mors=False): 30 | """ 31 | Collect properties for managed objects from a view ref 32 | 33 | Check the vSphere API documentation for example on retrieving 34 | object properties: 35 | 36 | - http://goo.gl/erbFDz 37 | 38 | Args: 39 | si (ServiceInstance): ServiceInstance connection 40 | view_ref (pyVmomi.vim.view.*): Starting point of inventory navigation 41 | obj_type (pyVmomi.vim.*): Type of managed object 42 | path_set (list): List of properties to retrieve 43 | include_mors (bool): If True include the managed objects 44 | refs in the result 45 | 46 | Returns: 47 | A list of properties for the managed objects 48 | 49 | """ 50 | collector = si.content.propertyCollector 51 | 52 | # Create object specification to define the starting point of 53 | # inventory navigation 54 | obj_spec = pyVmomi.vmodl.query.PropertyCollector.ObjectSpec() 55 | obj_spec.obj = view_ref 56 | obj_spec.skip = True 57 | 58 | # Create a traversal specification to identify the path for collection 59 | traversal_spec = pyVmomi.vmodl.query.PropertyCollector.TraversalSpec() 60 | traversal_spec.name = 'traverseEntities' 61 | traversal_spec.path = 'view' 62 | traversal_spec.skip = False 63 | traversal_spec.type = view_ref.__class__ 64 | obj_spec.selectSet = [traversal_spec] 65 | 66 | # Identify the properties to the retrieved 67 | property_spec = pyVmomi.vmodl.query.PropertyCollector.PropertySpec() 68 | property_spec.type = obj_type 69 | 70 | if not path_set: 71 | property_spec.all = True 72 | 73 | property_spec.pathSet = path_set 74 | 75 | # Add the object and property specification to the 76 | # property filter specification 77 | filter_spec = pyVmomi.vmodl.query.PropertyCollector.FilterSpec() 78 | filter_spec.objectSet = [obj_spec] 79 | filter_spec.propSet = [property_spec] 80 | 81 | # Retrieve properties 82 | props = collector.RetrieveContents([filter_spec]) 83 | 84 | data = [] 85 | for obj in props: 86 | properties = {} 87 | for prop in obj.propSet: 88 | properties[prop.name] = prop.val 89 | 90 | if include_mors: 91 | properties['obj'] = obj.obj 92 | 93 | data.append(properties) 94 | return data 95 | 96 | 97 | def get_container_view(si, obj_type, container=None): 98 | """ 99 | Get a vSphere Container View reference to all objects of type 'obj_type' 100 | 101 | It is up to the caller to take care of destroying the View when no longer 102 | needed. 103 | 104 | Args: 105 | obj_type (list): A list of managed object types 106 | 107 | Returns: 108 | A container view ref to the discovered managed objects 109 | """ 110 | if not container: 111 | container = si.content.rootFolder 112 | 113 | view_ref = si.content.viewManager.CreateContainerView( 114 | container=container, 115 | type=obj_type, 116 | recursive=True 117 | ) 118 | return view_ref 119 | 120 | 121 | def search_for_obj(content, vim_type, name, folder=None, recurse=True): 122 | """ 123 | Search the managed object for the name and type specified 124 | 125 | Sample Usage: 126 | 127 | get_obj(content, [vim.Datastore], "Datastore Name") 128 | """ 129 | if folder is None: 130 | folder = content.rootFolder 131 | 132 | obj = None 133 | container = content.viewManager.CreateContainerView(folder, vim_type, recurse) 134 | 135 | for managed_object_ref in container.view: 136 | if managed_object_ref.name == name: 137 | obj = managed_object_ref 138 | break 139 | container.Destroy() 140 | return obj 141 | 142 | 143 | def get_all_obj(content, vim_type, folder=None, recurse=True): 144 | """ 145 | Search the managed object for the name and type specified 146 | 147 | Sample Usage: 148 | 149 | get_obj(content, [vim.Datastore], "Datastore Name") 150 | """ 151 | if not folder: 152 | folder = content.rootFolder 153 | 154 | obj = {} 155 | container = content.viewManager.CreateContainerView(folder, vim_type, recurse) 156 | 157 | for managed_object_ref in container.view: 158 | obj[managed_object_ref] = managed_object_ref.name 159 | 160 | container.Destroy() 161 | return obj 162 | 163 | 164 | def get_obj(content, vim_type, name, folder=None, recurse=True): 165 | """ 166 | Retrieves the managed object for the name and type specified 167 | Throws an exception if of not found. 168 | 169 | Sample Usage: 170 | 171 | get_obj(content, [vim.Datastore], "Datastore Name") 172 | """ 173 | obj = search_for_obj(content, vim_type, name, folder, recurse) 174 | if not obj: 175 | raise RuntimeError("Managed Object " + name + " not found.") 176 | return obj 177 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/vmtools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | check for running vm tools 21 | """ 22 | 23 | __cmd__ = "vm-tools" 24 | 25 | import logging 26 | import re 27 | from collections import defaultdict 28 | 29 | from monplugin import Check, Status 30 | from pyVmomi import vim 31 | 32 | from ..tools import cli, service_instance 33 | from ..tools.helper import find_entity_views, isbanned, isallowed, CheckArgument 34 | from .. import CheckVsphereException 35 | 36 | check = None 37 | args = None 38 | 39 | 40 | def run(): 41 | global check 42 | global args 43 | 44 | parser = cli.Parser() 45 | parser.add_optional_arguments(cli.Argument.VIHOST) 46 | parser.add_optional_arguments(CheckArgument.BANNED( 47 | 'regex, checked against ' 48 | )) 49 | parser.add_optional_arguments(CheckArgument.ALLOWED( 50 | 'regex, checked against ' 51 | )) 52 | parser.add_optional_arguments({ 53 | 'name_or_flags': ['--old'], 54 | 'options': { 55 | 'action': 'store_true', 56 | 'default': False, 57 | 'help': 'tools that are too old generate a warning now', 58 | } 59 | }) 60 | parser.add_optional_arguments({ 61 | 'name_or_flags': ['--not-installed'], 62 | 'options': { 63 | 'action': 'store_true', 64 | 'default': False, 65 | 'help': 'tools not installed is ignored by default, make them critical', 66 | } 67 | }) 68 | parser.add_optional_arguments({ 69 | 'name_or_flags': ['-E', '--exclude-guest-id'], 70 | 'options': { 71 | 'action': 'append', 72 | 'default': [], 73 | 'help': "if config.guestId matches, VM is ignored", 74 | } 75 | }) 76 | args = parser.get_args() 77 | 78 | check = Check() 79 | 80 | args._si = service_instance.connect(args) 81 | 82 | if args.vihost: 83 | host_view = args._si.content.viewManager.CreateContainerView( 84 | args._si.content.rootFolder, [vim.HostSystem], True) 85 | try: 86 | parentView = next(x for x in host_view.view if x.name.lower() == args.vihost.lower()) 87 | except: 88 | raise CheckVsphereException(f"host {args.vihost} not found") 89 | else: 90 | parentView = args._si.content.rootFolder 91 | 92 | vms = find_entity_views( 93 | args._si, 94 | vim.VirtualMachine, 95 | begin_entity=parentView, 96 | properties=[ 97 | "name", 98 | "runtime.powerState", 99 | "summary.guest", 100 | "config.guestId", 101 | ], 102 | ) 103 | 104 | perf_data = defaultdict(int) 105 | vmscnt = len(vms) 106 | 107 | for vm in vms: 108 | name = vm["props"]["name"] 109 | isTemplate = vm["props"].get("config.template", None) 110 | guest_summary = vm["props"].get("summary.guest", None) 111 | powered = vm["props"].get("runtime.powerState", None) == "poweredOn" 112 | guestId = vm['props'].get('config.guestId', '') 113 | 114 | logging.debug(f"found VM {name} with guestId {guestId}") 115 | 116 | if isbanned(args, name): 117 | vmscnt -= 1 118 | continue 119 | if not isallowed(args, name): 120 | vmscnt -= 1 121 | continue 122 | 123 | if any(map( lambda p: re.search(p, guestId), args.exclude_guest_id )): 124 | vmscnt -= 1 125 | perf_data["excluded guestId"] += 1 126 | logging.debug(f"{name} matches {guestId} ~ {args.exclude_guest_id}") 127 | continue 128 | 129 | if isTemplate: 130 | perf_data["VM Templates"] += 1 131 | logging.debug(f"{name} is a template vm, ignoring ...") 132 | continue 133 | 134 | if not powered: 135 | perf_data["Powered off"] += 1 136 | logging.debug(f"{name} is powered off") 137 | continue 138 | 139 | 140 | if guest_summary: 141 | if guest_summary.toolsStatus == "toolsNotInstalled": 142 | perf_data["VMware Tools not installed"] += 1 143 | logging.debug(f"{name} has no vm tools installed") 144 | if args.not_installed: 145 | check.add_message(Status.CRITICAL, f"{name} tools not installed") 146 | elif guest_summary.toolsStatus == "toolsOld": 147 | perf_data["VMware Tools upgrade available"] += 1 148 | logging.debug(f"{name} tools upgrade available") 149 | if args.old: 150 | check.add_message(Status.WARNING, f"{name} tools upgrade available") 151 | elif guest_summary.toolsRunningStatus == "guestToolsNotRunning": 152 | perf_data["VMware Tools not running"] += 1 153 | check.add_message(Status.CRITICAL, f"{name} tools not running") 154 | logging.debug(f"{name} tools not running") 155 | else: 156 | perf_data["VMware Tools running"] += 1 157 | 158 | for key, value in perf_data.items(): 159 | check.add_perfdata(label=key, value=value, uom="") 160 | 161 | (code, message) = check.check_messages(separator="\n", separator_all="\n") 162 | check.exit( 163 | code=code, 164 | message=f"{vmscnt} VMs checked for VMware Tools state, {len(vms) - vmscnt} VMs ignored" 165 | if code == Status.OK 166 | else message, 167 | ) 168 | 169 | 170 | if __name__ == "__main__": 171 | try: 172 | run() 173 | except SystemExit as e: 174 | if e.code > 3 or e.code < 0: 175 | print("UNKNOWN EXIT CODE") 176 | raise SystemExit(Status.UNKNOWN) 177 | except Exception as e: 178 | print("UNKNOWN - " + str(e)) 179 | raise SystemExit(Status.UNKNOWN) 180 | -------------------------------------------------------------------------------- /checkvsphere/tools/helper.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | from pyVmomi import vim, vmodl 19 | from . import serviceutil 20 | 21 | 22 | # TODO: this might be slow, probably speed this up with 23 | def get_obj_by_name(si, vimtype, name): 24 | """ 25 | ex: get_obj_by_name(serviceinstance, vim.HostSystem, "foo.example.com") 26 | """ 27 | view = si.content.viewManager.CreateContainerView( 28 | si.content.rootFolder, 29 | [vimtype], 30 | True 31 | ) 32 | for obj in view.view: 33 | if obj.name == name: # TODO: maybe make this case insensitive? 34 | return obj 35 | 36 | return None 37 | 38 | def find_entity_views(service_instance, view_type, begin_entity=None, sieve=None, properties=None): 39 | """ 40 | find_entity_views(si, vim.HostSystem, sieve={"name": "esx1.vsphere.example.com"}) 41 | """ 42 | assert service_instance is not None 43 | assert view_type is not None 44 | if not begin_entity: 45 | begin_entity = service_instance.content.rootFolder 46 | if not sieve: 47 | sieve = dict() 48 | if not properties: 49 | properties = [] 50 | if view_type == vim.HostSystem: 51 | properties += ['runtime.inMaintenanceMode', 'runtime.powerState'] 52 | if view_type == vim.VirtualMachine: 53 | properties += ['runtime.powerState'] 54 | assert isinstance(sieve, dict) 55 | assert isinstance(properties, list) 56 | 57 | propertySpec = vmodl.query.PropertyCollector.PropertySpec( 58 | pathSet=list(sieve.keys()) + properties, 59 | type=view_type, 60 | all=False 61 | ) 62 | 63 | property_filter_spec = get_search_filter_spec(begin_entity, [propertySpec]) 64 | obj_contents = service_instance.content.propertyCollector.RetrieveContents([property_filter_spec]) 65 | 66 | filtered_objs = [] 67 | 68 | for obj in obj_contents: 69 | props = {} 70 | for p in obj.propSet: 71 | props[p.name] = p.val 72 | 73 | if not sieve: 74 | filtered_objs.append({"obj": obj, "props": props}) 75 | continue 76 | else: # FIXME: implement sieve here, currently it only works with one search pattern 77 | 78 | matched = True 79 | 80 | try: 81 | for property_name, property_value in sieve.items(): 82 | #print((property_name, property_value, props)) 83 | if property_name in props: 84 | if props[property_name] != property_value: 85 | raise Exception("Not Matched") 86 | except: 87 | matched = False 88 | 89 | if matched: 90 | filtered_objs.append({"obj": obj, "props": props}) 91 | 92 | return filtered_objs 93 | # return service_instance.content.viewManager.CreateListView( obj = filtered_objs ) 94 | 95 | 96 | def get_search_filter_spec(begin_entity, property_specs): 97 | # What's wrong with you VMWARE? 98 | fullspec = serviceutil.build_full_traversal() 99 | ObjectSpec = vmodl.query.PropertyCollector.ObjectSpec 100 | FilterSpec = vmodl.query.PropertyCollector.FilterSpec 101 | 102 | obj_spec = ObjectSpec( 103 | obj=begin_entity, 104 | skip=False, 105 | selectSet=fullspec 106 | ) 107 | 108 | return FilterSpec( 109 | propSet=property_specs, 110 | objectSet=[obj_spec], 111 | ) 112 | 113 | 114 | def get_metric(perfMgr, perfCounterStr, perfInstance): 115 | for counter in perfMgr.perfCounter: 116 | if f'{counter.groupInfo.key}:{counter.nameInfo.key}:{counter.rollupType}' == perfCounterStr: 117 | return (counter, vim.PerformanceManager.MetricId( 118 | counterId=counter.key, 119 | instance=perfInstance 120 | )) 121 | return (None, None) 122 | 123 | 124 | class CheckArgument: 125 | def __init__(self): 126 | pass 127 | 128 | VIMNAME = { 129 | 'name_or_flags': ['--vimname'], 130 | 'options': {'action': 'store', 'help': 'name of the vimtype object'}, 131 | } 132 | 133 | VIMTYPE = { 134 | 'name_or_flags': ['--vimtype'], 135 | 'options': { 136 | 'action': 'store', 137 | 'help': 'the object type to check, i.e. HostSystem, Datacenter or VirtualMachine', 138 | }, 139 | } 140 | 141 | WARNING_THRESHOLD = { 142 | 'name_or_flags': ['--warning'], 143 | 'options': {'action': 'store', 'help': 'warning threshold'}, 144 | } 145 | 146 | CRITICAL_THRESHOLD = { 147 | 'name_or_flags': ['--critical'], 148 | 'options': {'action': 'store', 'help': 'critical threshold'}, 149 | } 150 | 151 | def ALLOWED(help, name=None): 152 | if not name: 153 | name = ['--allowed', '--include'] 154 | return { 155 | 'name_or_flags': name, 156 | 'options': { 157 | 'default': [], 158 | 'help': help, 159 | 'action': 'append', 160 | } 161 | } 162 | def BANNED(help, name=None): 163 | if not name: 164 | name = ['--banned', '--exclude'] 165 | return { 166 | 'name_or_flags': name, 167 | 'options': { 168 | 'default': [], 169 | 'help': help, 170 | 'action': 'append', 171 | } 172 | } 173 | 174 | def match_method(args): 175 | return getattr(args, 'match_method', "search") 176 | 177 | def isbanned(args, name, attr="banned"): 178 | """ 179 | checks name against regexes in args.banned 180 | """ 181 | banned = getattr(args, attr) 182 | if banned: 183 | for pattern in banned: 184 | p = re.compile(pattern) 185 | if getattr(p, match_method(args))(name): 186 | return True 187 | 188 | return False 189 | 190 | def isallowed(args, name, attr="allowed"): 191 | """ 192 | checks name against regexes in args.allowed 193 | """ 194 | allowed = getattr(args, attr, None) 195 | if allowed: 196 | for pattern in allowed: 197 | p = re.compile(pattern) 198 | if getattr(p, match_method(args))(name): 199 | return True 200 | return False 201 | 202 | return True 203 | 204 | def process_retrieve_content(content): 205 | """ 206 | reorganize RetrieveContents shit, so we can use it. 207 | """ 208 | objs = [] 209 | for o in content: 210 | d = {} 211 | d['moref'] = o.obj 212 | for prop in o.propSet: 213 | d[prop.name] = prop.val 214 | objs.append(d) 215 | return objs 216 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/datastores.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | checks capacity of datastores 20 | """ 21 | 22 | __cmd__ = 'datastores' 23 | 24 | from pyVmomi import vim, vmodl 25 | from monplugin import Check, Status, Threshold, Range 26 | from ..tools import cli, service_instance 27 | from ..tools.helper import ( 28 | CheckArgument, 29 | find_entity_views, 30 | isallowed, 31 | isbanned, 32 | process_retrieve_content 33 | ) 34 | 35 | class Space: 36 | def __init__(self, capacity, free): 37 | self.capacity = capacity 38 | self.free = free 39 | self.used = capacity - free 40 | self.usage = 100 * self.used / capacity 41 | 42 | def __getitem__(self, key): 43 | unit = 'B' 44 | if "_" in key: 45 | (key, unit) = key.split('_') 46 | 47 | # Don't do conversion on usage 48 | if key == "usage": 49 | return self.__dict__[key] 50 | 51 | return self.__dict__[key] / Space.conversion_table[unit] 52 | 53 | conversion_table = { 54 | '%': 1, 55 | 'B': 1, 56 | 'kB': 2**10, 57 | 'MB': 2**20, 58 | 'GB': 2**30, 59 | } 60 | 61 | def range_in_bytes(r: Range, uom): 62 | start = r.start 63 | end = r.end 64 | 65 | start *= Space.conversion_table[uom] 66 | end *= Space.conversion_table[uom] 67 | 68 | return ('' if r.outside else '@') + \ 69 | ('~' if start == float('-inf') else str(start)) + \ 70 | ":" + ('' if end == float('+inf') else str(end)) 71 | 72 | args = None 73 | 74 | def run(): 75 | global args 76 | parser = cli.Parser() 77 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of datastore')) 78 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of datastore')) 79 | parser.add_optional_arguments(CheckArgument.VIMNAME) 80 | parser.add_optional_arguments(CheckArgument.VIMTYPE) 81 | parser.add_optional_arguments(CheckArgument.CRITICAL_THRESHOLD) 82 | parser.add_optional_arguments(CheckArgument.WARNING_THRESHOLD) 83 | parser.add_optional_arguments({ 84 | 'name_or_flags': ['--metric'], 85 | 'options': { 86 | 'action': 'store', 87 | 'default': 'usage', 88 | 'help': 'The metric to apply the thresholds on, defaults to `usage`, can be: ' 89 | 'usage (in percent), free and used. ' 90 | 'free and used are measured in bytes. You can one of these suffixes: ' 91 | 'kB, MB, GB for example: free_MB or used_GB' 92 | } 93 | }) 94 | args = parser.get_args() 95 | 96 | si = service_instance.connect(args) 97 | check = Check(threshold = Threshold(args.warning or None, args.critical or None)) 98 | 99 | vimtype = None 100 | if args.vimtype: 101 | vimtype = getattr(vim, args.vimtype) 102 | 103 | datastores = [] 104 | 105 | if vimtype in [vim.HostSystem, vim.ClusterComputeResource, vim.Datacenter]: 106 | try: 107 | vm = find_entity_views( 108 | si, 109 | vimtype, 110 | begin_entity=si.content.rootFolder, 111 | sieve=( {'name': args.vimname} if args.vimname else None ), 112 | properties=["name", "datastore"], 113 | )[0] 114 | datastores = vm['props']['datastore'] 115 | except IndexError: 116 | check.exit(Status.UNKNOWN, f"{args.vimtype} {args.vimname} not found") 117 | else: 118 | dcs = find_entity_views( 119 | si, 120 | vim.Datacenter, 121 | begin_entity=si.content.rootFolder, 122 | properties=["datastore"], 123 | ) 124 | for dc in dcs: 125 | datastores.extend(dc['props']['datastore']) 126 | 127 | datastore_info(check, si, datastores) 128 | 129 | def datastore_info(check: Check, si: vim.ServiceInstance, datastores): 130 | ObjectSpec = vmodl.query.PropertyCollector.ObjectSpec 131 | retrieve = si.content.propertyCollector.RetrieveContents 132 | propspec = vmodl.query.PropertyCollector.PropertySpec( 133 | all=False, 134 | pathSet=['summary', 'info'], 135 | type=vim.Datastore 136 | ) 137 | 138 | objs = [] 139 | for store in datastores: 140 | objs.append(ObjectSpec(obj=store)) 141 | 142 | filter_spec = vmodl.query.PropertyCollector.FilterSpec( 143 | objectSet = objs, 144 | propSet = [propspec], 145 | ) 146 | 147 | result = retrieve( [filter_spec] ) 148 | stores = process_retrieve_content(result) 149 | stores_count = len(stores) 150 | filtered = False 151 | 152 | for store in stores: 153 | name = f"{ store['moref']._moId }_{store['summary'].name}" 154 | datastore_type = store['summary'].type 155 | 156 | if isbanned(args, f"{name}"): 157 | stores_count -= 1 158 | continue 159 | if not isallowed(args, f"{name}"): 160 | stores_count -= 1 161 | filtered = True 162 | continue 163 | 164 | if not store['summary'].accessible: 165 | check.add_message(Status.CRITICAL, f"{name} is not accessible") 166 | continue 167 | 168 | try: 169 | space = Space(store['summary'].capacity, store['summary'].freeSpace) 170 | except ZeroDivisionError: 171 | check.add_message(Status.CRITICAL, f"{name} has a capacity of zero") 172 | continue 173 | 174 | 175 | for metric in ['usage', 'free', 'used', 'capacity']: 176 | opts = {} 177 | 178 | # Check threshold against this metric 179 | if args.metric.startswith(metric) and (args.warning or args.critical): 180 | value = space[args.metric] 181 | _, uom, *_ = (args.metric.split('_') + ['%' if 'usage' in args.metric else 'B']) 182 | s = check.threshold.get_status(space[args.metric]) 183 | 184 | threshold = {} 185 | opts['threshold'] = {} 186 | if args.warning: 187 | threshold['warning'] = range_in_bytes(Range(args.warning), uom) 188 | if args.critical: 189 | threshold['critical'] = range_in_bytes(Range(args.critical), uom) 190 | opts['threshold'] = Threshold(**threshold) 191 | 192 | if s != Status.OK: 193 | check.add_message(s, f"{args.metric} on {name} is in state {s.name}: {value :.2f}{uom}") 194 | 195 | puom = '%' if metric == 'usage' else 'B' 196 | check.add_perfdata(label=f"{name} {metric}", value=space[metric], uom=puom, **opts) 197 | 198 | if filtered and not stores_count: 199 | check.add_message(Status.WARNING, "no datastores found") 200 | 201 | (code, message) = check.check_messages(separator="\n")#, allok=okmessage) 202 | check.exit( 203 | code=code, 204 | message=( message or "everything ok" ) 205 | ) 206 | 207 | 208 | if __name__ == "__main__": 209 | run() 210 | -------------------------------------------------------------------------------- /docs/cmd/perf.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: perf 3 | --- 4 | 5 | ## Description 6 | 7 | The perf command extracts a values from a 8 | [_PerfCounter_](https://dp-downloads.broadcom.com/api-content/apis/API_VWSA_001/8.0U3/html/ReferenceGuides/vim.PerformanceManager.html). 9 | You can get a list of all available counters with a short description (as 10 | provided by the API) with the [list-metrics](../list-metrics) command. 11 | 12 | ## Options 13 | 14 | Besides the [general options](../../general-options) this command supports the following 15 | options: 16 | 17 | | option | description | 18 | |---|---| 19 | | `--vimtype VIMTYPE` | the object type to check, see [common options](../../general-options/#common-options) | 20 | | `--vimname VIMNAME` | name of the vimtype object, see [common options](../../general-options/#common-options) | 21 | | `--critical CRITICAL` | warning threshold, see [common options](../../general-options/#common-options) | 22 | | `--warning WARNING` | warning threshold, see [common options](../../general-options/#common-options) | 23 | | `--perfcounter PERFCOUNTER` | a colon separated string composed of groupInfo.key:nameInfo.key:rollupType | 24 | | `--perfinstance PERFINSTANCE` | the instance of of the metric to monitor.
defaults to empty string, which is not always available but means an aggregated value over all instances.
Can also be `*` meaning all instances, in this case the threshold is checked against each of the instances | 25 | | `--maintenance-state` | exit state if the host is in maintenance,
one of `OK, WARNING, CRITICAL, UNKNOWN` (only has a meaning with `--vimtype HostSystem` | 26 | | `--interval INTERVALID` | defaults to `20` which works in most cases, other possible values `300, 1800, 7200, 86400` and maybe more ...| 27 | 28 | In case of `--vimtype HostSystem` it may be useful to omit the `--vimname` when 29 | you run this command directly against the HostSystem (not through the vcenter). 30 | 31 | ## Examples 32 | 33 | ``` 34 | # check for too much cpu usage 35 | check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 36 | --vimtype HostSystem --vimname esx1.int.example.com \ 37 | --perfcounter cpu:usage:average --perfinstance '' \ 38 | --critical 80 39 | 40 | # check for too less cpu usage (application died?) 41 | check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 42 | --vimtype HostSystem --vimname esx1.int.example.com \ 43 | --perfcounter cpu:usage:average \ 44 | --critical 5: 45 | 46 | # check if there was a reboot within the last 10 minutes 47 | check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 48 | --vimtype HostSystem --vimname esx1.int.example.com \ 49 | --perfcounter sys:uptime:latest \ 50 | --critical 600: 51 | 52 | # check disk latency 53 | $ check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 54 | --vimname esx1.int.example.com --vimtype HostSystem \ 55 | --perfcounter disk:totalLatency:average 56 | UNKNOWN: Cannot find disk:totalLatency:average for the queried resources 57 | 58 | # On that error you may want to try --perfinstance '*' 59 | 60 | $ check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 61 | --vimname esx1.int.example.com --vimtype HostSystem \ 62 | --perfcounter disk:totalLatency:average --perfinstance '*' 63 | OK: disk:totalLatency:average_naa.6000eb3810d426400000000000000277 has value 0 Millisecond 64 | disk:totalLatency:average_naa.600605b00ba8cb0022564867b8c8cc32 has value 2 Millisecond 65 | disk:totalLatency:average_naa.6000eb3810d4264000000000000000b2 has value 0 Millisecond 66 | disk:totalLatency:average_naa.600605b00ba8cb001fd947850523e56d has value 0 Millisecond 67 | disk:totalLatency:average_naa.600605b00ba8cb0029700b163217244e has value 6 Millisecond 68 | disk:totalLatency:average_naa.6000eb3810d4264000000000000002b3 has value 1 Millisecond 69 | | 'disk:totalLatency:average_naa.6000eb3810d426400000000000000277'=0.0ms;;;; 70 | 'disk:totalLatency:average_naa.600605b00ba8cb0022564867b8c8cc32'=2.0ms;;;; 71 | ... 72 | 73 | $ check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 74 | --vimname esx1.int.example.com --vimtype HostSystem \ 75 | --perfcounter disk:totalLatency:average --perfinstance 'naa.600605b00ba8cb0022564867b8c8cc32' 76 | 77 | $ check_vsphere perf -s vcenter.example.com -u naemon@vsphere.local -nossl \ 78 | --vimname esx1.int.example.com --vimtype HostSystem --perfcounter power:power:average 79 | OK: Counter power:power:average on HostSystem:esx1.int.example.com reported 227 Watt 80 | | 'power:power:average'=227.0;;;; 81 | ``` 82 | 83 | ## Rosetta 84 | 85 | | check\_vmware\_esx | check\_vsphere | vimtypes | 86 | |---|---|---| 87 | | `--select cpu --subselect usage` | `perf --perfcounter cpu:usage:average` | HostSystem, VirtualMachine | 88 | | `--select cpu --subselect ready` | `perf --perfcounter cpu:ready:summation` | HostSystem, VirtualMachine | 89 | | `--select cpu --subselect wait` | `perf --perfcounter cpu:wait:summation` | HostSystem, VirtualMachine | 90 | | `--select mem --subselect usage` | `perf --perfcounter mem:usage:average` | HostSystem, VirtualMachine | 91 | | `--select mem --subselect consumed` | `perf --perfcounter mem:consumed:average` | HostSystem, VirtualMachine | 92 | | `--select mem --subselect swapused` | `perf --perfcounter mem:swapused:average` | HostSystem, VirtualMachine | 93 | | `--select mem --subselect overhead` | `perf --perfcounter mem:overhead:average` | HostSystem, VirtualMachine | 94 | | `--select mem --subselect memctl` | `perf --perfcounter mem:vmmemctl:average` | HostSystem, VirtualMachine | 95 | | `--select io --subselect aborted` | `perf --perfcounter disk:commandsAborted:summation` | HostSystem, VirtualMachine | 96 | | `--select io --subselect resets` | `perf --perfcounter disk:busResets:summation` | HostSystem, VirtualMachine | 97 | | `--select io --subselect read` | `perf --perfcounter disk:read:average` | HostSystem, VirtualMachine | 98 | | `--select io --subselect read_latency` | `perf --perfcounter disk:totalReadLatency:average` | HostSystem, VirtualMachine | 99 | | `--select io --subselect write` | `perf --perfcounter disk:write:average` | HostSystem, VirtualMachine | 100 | | `--select io --subselect write_latency` | `perf --perfcounter disk:totalWriteLatency:average` | HostSystem, VirtualMachine | 101 | | `--select io --subselect usage` | `perf --perfcounter disk:usage:average` | HostSystem, VirtualMachine | 102 | | `--select io --subselect kernel_latency` | `perf --perfcounter disk:kernelLatency:average` | HostSystem, VirtualMachine | 103 | | `--select io --subselect device_latency` | `perf --perfcounter disk:deviceLatency:average` | HostSystem, VirtualMachine | 104 | | `--select io --subselect queue_latency` | `perf --perfcounter disk:queueLatency:average` | HostSystem, VirtualMachine | 105 | | `--select io --subselect total_latency` | `perf --perfcounter disk:totalLatency:average` | HostSystem, VirtualMachine | 106 | | `--select net --subselect usage` | `perf --perfcounter net:usage:average` | HostSystem, VirtualMachine | 107 | | `--select net --subselect receive` | `perf --perfcounter net:received:average` | HostSystem, VirtualMachine | 108 | | `--select net --subselect send` | `perf --perfcounter net:transmitted:average` | HostSystem, VirtualMachine | 109 | | `--select uptime` | `perf --perfcounter sys:uptime:latest` | HostSystem,VirtualMachine | 110 | | `--select cluster --subselect effectivecpu` | `perf --perfcounter clusterservices:effectivecpu:average` | ClusterComputeResource | 111 | | `--select cluster --subselect effectivemem` | `perf --perfcounter clusterservices:effectivemem:average` | ClusterComputeResource | 112 | | `--select cluster --subselect failover` | `perf --perfcounter clusterservices:failover:latest` | ClusterComputeResource | 113 | | `--select cluster --subselect cpufairness` | `perf --perfcounter clusterservices:cpufairness:latest` | ClusterComputeResource | 114 | | `--select cluster --subselect memfairness` | `perf --perfcounter clusterservices:memfairness:latest` | ClusterComputeResource | 115 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/perf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | check performance values from Vsphere 21 | """ 22 | 23 | __cmd__ = 'perf' 24 | 25 | from pyVmomi import vim 26 | from ..tools import cli, service_instance 27 | from ..tools.helper import find_entity_views, get_metric, CheckArgument 28 | from .. import CheckVsphereException 29 | from monplugin import Check, Status 30 | 31 | ''' 32 | [kiloBytes] 33 | [megaBytes] 34 | [teraBytes] 35 | [percent] 36 | [microsecond] 37 | [millisecond] 38 | [second] 39 | [celsius] 40 | [joule] 41 | [kiloBytesPerSecond] 42 | [megaHertz] 43 | [number] 44 | [watt] 45 | ''' 46 | 47 | 48 | def get_counter_info(counter): 49 | info = {} 50 | info['factor'] = 1 51 | info['unit'] = counter.unitInfo.summary 52 | info['perfUnit'] = None 53 | unit = counter.unitInfo.key 54 | if unit == 'percent': 55 | # percent is actually ‱ (permyriad) 56 | info['factor'] = 0.01 57 | info['unit'] = '%' 58 | info['perfUnit'] = '%' 59 | elif unit.endswith('Bytes'): 60 | unit = unit.capitalize() 61 | info['perfUnit'] = unit[0] + 'B' 62 | info['unit'] = info['perfUnit'] + 'ytes' 63 | elif unit.endswith('second'): 64 | if unit.startswith('milli'): 65 | info['perfUnit'] = 'ms' 66 | elif unit.startswith('micro'): 67 | info['perfUnit'] = 'us' 68 | elif unit == 'second': 69 | info['perfUnit'] = 's' 70 | elif unit.endswith('number'): 71 | info['unit'] = '' 72 | 73 | return info 74 | 75 | 76 | def run(): 77 | parser = get_argparser() 78 | args = parser.get_args() 79 | 80 | check = Check() 81 | check.set_threshold(warning=args.warning, critical=args.critical) 82 | 83 | args._si = service_instance.connect(args) 84 | 85 | try: 86 | vimtype = getattr(vim, args.vimtype) 87 | except: 88 | raise Exception(f"vim.{args.vimtype} is not known") 89 | 90 | try: 91 | args.perfcounter.split(":", 2) 92 | except: 93 | raise Exception("perfcounter must be composed as groupName:perfName:rollupType") 94 | 95 | (counter, metricId) = get_metric( 96 | args._si.content.perfManager, args.perfcounter, args.perfinstance 97 | ) 98 | 99 | # I hate you so much vmware 100 | # https://vdc-download.vmware.com/vmwb-repository/dcr-public/bf660c0a-f060-46e8-a94d-4b5e6ffc77ad/208bc706-e281-49b6-a0ce-b402ec19ef82/SDK/vsphere-ws/docs/ReferenceGuide/cpu_counters.html 101 | 102 | vms = find_entity_views( 103 | args._si, 104 | vimtype, 105 | properties=['name'], 106 | begin_entity=args._si.content.rootFolder, 107 | sieve=( {'name': args.vimname} if args.vimname else None ) 108 | ) 109 | 110 | try: 111 | obj = vms[0]['obj'].obj 112 | props = vms[0]['props'] 113 | except IndexError: 114 | check.exit(Status.UNKNOWN, f"{args.vimtype} {args.vimname or ''} not found") 115 | 116 | if not metricId: 117 | raise Exception( 118 | f"metric not found by {args.perfcounter}:{args.perfinstance}, " 119 | "maybe --perfinstance='*' helps to examine the available instances" 120 | ) 121 | if not obj: 122 | raise Exception(f"vim.{args.vimtype} not found with name {args.vimname}") 123 | 124 | if 'runtime.inMaintenanceMode' in props: 125 | if props['runtime.inMaintenanceMode']: 126 | check.exit( 127 | Status[args.maintenance_state], 128 | f"{args.vimname or props['name']} is in maintenance" 129 | ) 130 | 131 | counterInfo = get_counter_info(counter) 132 | 133 | try: 134 | values = get_perf_values(args, obj, metricId)[0] 135 | except IndexError: 136 | check.exit(Status.UNKNOWN, f"Cannot find {args.perfcounter} for the queried resources") 137 | 138 | if not values.value: 139 | check.exit(code=Status.UNKNOWN, message="No data returned") 140 | 141 | if args.perfinstance == '': 142 | for instance in values.value: 143 | val = instance.value[0] * counterInfo['factor'] 144 | if instance.id.instance == args.perfinstance: 145 | check.add_perfdata( 146 | label=args.perfcounter, 147 | value=val, 148 | threshold=check.threshold, 149 | uom=counterInfo['perfUnit'], 150 | ) 151 | check.exit( 152 | code=check.check_threshold(val), 153 | message=f'Counter {args.perfcounter} on {args.vimtype}:{args.vimname or props["name"]} reported {val:.8g} {counterInfo["unit"]}', 154 | ) 155 | else: 156 | metrics_count = len(values.value) 157 | for instance in values.value: 158 | if metrics_count == 1 and instance.id.instance == '': 159 | raise CheckVsphereException("The perfcounter only returns an aggregate, maybe you want to use --perfinstance '' instead") 160 | if instance.id.instance == '': 161 | # ignore the aggregate if we query a specific or all instances 162 | continue 163 | if args.perfinstance == '*' or args.perfinstance == instance.id.instance: 164 | val = instance.value[0] * counterInfo['factor'] 165 | check.add_perfdata( 166 | label=f'{instance.id.instance} {args.perfcounter}', 167 | value=val, 168 | threshold=check.threshold, 169 | uom=counterInfo['perfUnit'], 170 | ) 171 | check.add_message( 172 | check.threshold.get_status(val), 173 | f"'{instance.id.instance} {args.perfcounter}' has value {val:.8g} {counterInfo['unit']}", 174 | ) 175 | 176 | (code, message) = check.check_messages(separator='\n ') 177 | check.exit(code=code, message=message) 178 | 179 | 180 | def get_perf_values(args, obj, metricId): 181 | si = args._si 182 | 183 | perfMgr = si.content.perfManager 184 | 185 | perfQuerySpec = [] 186 | perfQuerySpec.append( 187 | vim.PerformanceManager.QuerySpec( 188 | maxSample=1, 189 | entity=obj, 190 | metricId=[metricId], 191 | intervalId=args.interval, 192 | ) 193 | ) 194 | 195 | perfData = perfMgr.QueryPerf(querySpec=perfQuerySpec) 196 | return perfData 197 | 198 | 199 | def get_argparser(): 200 | parser = cli.Parser() 201 | 202 | parser.add_optional_arguments( CheckArgument.CRITICAL_THRESHOLD ) 203 | parser.add_optional_arguments( CheckArgument.WARNING_THRESHOLD ) 204 | parser.add_optional_arguments( { 205 | 'name_or_flags': ['--maintenance-state'], 206 | 'options': { 207 | 'action': 'store', 208 | 'choices': ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'], 209 | 'default': 'UNKNOWN', 210 | 'help': 'exit with this status if the host is in maintenance, only does something with --vimtype HostSystem' 211 | } 212 | }) 213 | 214 | parser.add_required_arguments( CheckArgument.VIMTYPE ) 215 | parser.add_optional_arguments( CheckArgument.VIMNAME ) 216 | 217 | parser.add_required_arguments( 218 | { 219 | 'name_or_flags': ['--perfcounter'], 220 | 'options': { 221 | 'action': 'store', 222 | 'help': 'a colon separated string composed of groupInfo.key:nameInfo.key:rollupType', 223 | }, 224 | } 225 | ) 226 | 227 | parser.add_optional_arguments( 228 | { 229 | 'name_or_flags': ['--perfinstance'], 230 | 'options': { 231 | 'action': 'store', 232 | 'default': '', 233 | 'help': 'the instance of of the metric to monitor. defaults to empty string, ' 234 | 'which is not always available but means an aggregated value over all instances', 235 | }, 236 | } 237 | ) 238 | parser.add_optional_arguments( 239 | { 240 | 'name_or_flags': ['--interval'], 241 | 'options': { 242 | 'action': 'store', 243 | 'type': int, 244 | 'default': 20, 245 | 'help': 'The interval (in seconds) to aggregate over', 246 | }, 247 | } 248 | ) 249 | 250 | return parser 251 | 252 | 253 | if __name__ == "__main__": 254 | try: 255 | run() 256 | except SystemExit as e: 257 | if not isinstance(e.code, int) or not (0 <= e.code <= 3): 258 | print("UNKNOWN EXIT CODE") 259 | raise SystemExit(Status.UNKNOWN) 260 | except Exception as e: 261 | print("UNKNOWN - " + str(e)) 262 | raise SystemExit(Status.UNKNOWN) 263 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/hoststorage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | checks storage adapters 20 | """ 21 | 22 | __cmd__ = 'host-storage' 23 | 24 | import re 25 | import itertools 26 | from pyVmomi import vim, vmodl 27 | from monplugin import Check, Status 28 | from collections import namedtuple 29 | 30 | from ..tools import cli, service_instance 31 | from ..tools.helper import ( 32 | CheckArgument, 33 | find_entity_views, 34 | isallowed, 35 | isbanned, 36 | process_retrieve_content 37 | ) 38 | 39 | args = None 40 | 41 | def run(): 42 | global args 43 | parser = cli.Parser() 44 | #parser.add_optional_arguments(CheckArgument.CRITICAL_THRESHOLD) 45 | #parser.add_optional_arguments(CheckArgument.WARNING_THRESHOLD) 46 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of datastore')) 47 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of datastore')) 48 | parser.add_optional_arguments(cli.Argument.VIHOST) 49 | parser.add_required_arguments( { 50 | 'name_or_flags': ['--mode'], 51 | 'options': { 52 | 'action': 'store', 53 | 'choices': [ 54 | 'adapter', 55 | 'lun', 56 | 'path', 57 | ], 58 | 'help': 'which runtime mode to check' 59 | } 60 | }) 61 | parser.add_optional_arguments({ 62 | 'name_or_flags': ['--maintenance-state'], 63 | 'options': { 64 | 'action': 'store', 65 | 'default': 'UNKNOWN', 66 | 'choices': ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'], 67 | 'help': 'exit with this status if the host is in maintenance, ' 68 | 'default UNKNOWN, or CRITICAL if --mode maintenance' 69 | } 70 | }) 71 | args = parser.get_args() 72 | 73 | si = service_instance.connect(args) 74 | check = Check() 75 | 76 | try: 77 | host = find_entity_views( 78 | si, 79 | vim.HostSystem, 80 | begin_entity=si.content.rootFolder, 81 | sieve=( {'name': args.vihost} if args.vihost else None ), 82 | properties=["name", "configManager", "runtime.inMaintenanceMode"], 83 | )[0] 84 | except IndexError: 85 | check.exit(Status.UNKNOWN, f"host {args.vihost or ''} not found") 86 | 87 | if host['props']['runtime.inMaintenanceMode']: 88 | check.exit( 89 | Status[args.maintenance_state], 90 | f"host {host['props']['name']} is in maintenance" 91 | ) 92 | 93 | storage = storage_info(si, host) 94 | 95 | if args.mode == "adapter": 96 | check_adapter(check, args, storage) 97 | elif args.mode == "lun": 98 | check_lun(check, args, storage) 99 | elif args.mode == "path": 100 | check_path(check, args, storage) 101 | 102 | def get_lun2disc(storage): 103 | lun2disc = {} 104 | 105 | for adapter in storage['storageDeviceInfo'].scsiTopology.adapter: 106 | for target in adapter.target: 107 | for lun in target.lun: 108 | key = lun.scsiLun 109 | key = key.split("-")[-1] 110 | lun2disc[key] = f"{lun.lun :03d}" 111 | 112 | return lun2disc 113 | 114 | 115 | def check_path(check: Check, si: vim.ServiceInstance, storage): 116 | lun2disc = get_lun2disc(storage) 117 | PathInfo = namedtuple('PathInfo', ('scsiId', 'path', 'lun')) 118 | 119 | def iter_lun(): 120 | for mpInfoLun in storage['storageDeviceInfo'].multipathInfo.lun: 121 | for path in mpInfoLun.path: 122 | scsiId = path.lun.split("-")[-1] 123 | yield PathInfo(scsiId, path, lun2disc[scsiId]) 124 | 125 | groupedPaths = itertools.groupby( 126 | sorted( 127 | list(iter_lun()), 128 | key=lambda x: x.scsiId 129 | ), 130 | lambda x: x.scsiId 131 | ) 132 | 133 | for scsiId, pathinfos in groupedPaths: 134 | for pi in pathinfos: 135 | if pi.path.state not in [ 'active', 'disabled', 'standby' ]: 136 | check.add_message( 137 | Status.CRITICAL, 138 | f"On LUN {pi.lun} the path {pi.path.name} is in state {pi.path.state}" 139 | ) 140 | else: 141 | check.add_message( 142 | Status.OK, 143 | f"On LUN {pi.lun} the path {pi.path.name} is in state {pi.path.state}" 144 | ) 145 | 146 | 147 | (code, message) = check.check_messages(separator='\n', separator_all="\n", allok="All paths are OK") 148 | prefix = "The following paths are faulty:\n" 149 | check.exit( 150 | code=code, 151 | message=f"{'' if code == Status.OK else prefix}{message}" 152 | ) 153 | 154 | def check_lun(check: Check, si: vim.ServiceInstance, storage): 155 | lun2disc = get_lun2disc(storage) 156 | count: dict[str, int] = {} 157 | luns = storage['storageDeviceInfo'].scsiLun 158 | for scsi in luns: 159 | discKey = scsi.key.split("-")[-1] 160 | displayName = re.sub(r'[^][\w _().-]', '', scsi.displayName) 161 | 162 | if isbanned(args, displayName): 163 | count.setdefault('ignored', 0) 164 | count['ignored'] += 1 165 | continue 166 | if not isallowed(args, displayName): 167 | count.setdefault('ignored', 0) 168 | count['ignored'] += 1 169 | continue 170 | 171 | operationState = "-".join(scsi.operationalState) 172 | if "degraded" in scsi.operationalState: 173 | check.add_message(Status.WARNING, f"WARNING LUN:{lun2disc[discKey]} {displayName} degraded: {operationState}") 174 | count.setdefault('warning', 0) 175 | count['warning'] += 1 176 | elif "ok" == scsi.operationalState[0]: 177 | check.add_message(Status.OK, f"OK LUN:{lun2disc[discKey]} {displayName} state: {operationState}") 178 | count.setdefault('ok', 0) 179 | count['ok'] += 1 180 | else: 181 | check.add_message(Status.CRITICAL, f"CRITICAL LUN:{lun2disc[discKey]} {displayName} state: {operationState}") 182 | count.setdefault('critical', 0) 183 | count['critical'] += 1 184 | 185 | (code, message) = check.check_messages(separator='\n', separator_all="\n")#, allok=okmessage) 186 | short = f"LUNs: {len(luns)}; " + "; ".join([ f"{x}: {count[x]}" for x in sorted(count.keys()) ]) 187 | check.exit( 188 | code=code, 189 | message=f"{short}\n{message}" 190 | ) 191 | 192 | 193 | 194 | def check_adapter(check: Check, si: vim.ServiceInstance, storage): 195 | count: dict[str, int] = {} 196 | adapters = storage['storageDeviceInfo'].hostBusAdapter 197 | for dev in adapters: 198 | if ( 199 | isbanned(args, f"device:{dev.device}") or \ 200 | isbanned(args, f"model:{dev.model}") or \ 201 | isbanned(args, f"key:{dev.key}") 202 | ): 203 | count.setdefault('ignored', 0) 204 | count['ignored'] += 1 205 | continue 206 | if not ( 207 | isallowed(args, f"device:{dev.device}") or \ 208 | isallowed(args, f"model:{dev.model}") or \ 209 | isallowed(args, f"key:{dev.key}") 210 | ): 211 | count.setdefault('ignored', 0) 212 | count['ignored'] += 1 213 | continue 214 | 215 | status = { 216 | 'online': Status.OK, 217 | 'unbound': Status.WARNING, 218 | 'unknown': Status.CRITICAL, 219 | 'offline': Status.CRITICAL, 220 | }.get(dev.status, Status.UNKNOWN) 221 | count.setdefault(dev.status, 0) 222 | count[dev.status]+=1 223 | check.add_message(status, f"{dev.model} {dev.device} ({dev.status})") 224 | 225 | short = f"Adapters {len(adapters)}; " + "; ".join([f"{x}: {count[x]}" for x in sorted(count.keys())]) 226 | (code, message) = check.check_messages(separator="\n", separator_all="\n")#, allok=okmessage) 227 | check.exit( 228 | code=code, 229 | message=f"{short}\n{message}" 230 | ) 231 | 232 | def storage_info(si: vim.ServiceInstance, host): 233 | ObjectSpec = vmodl.query.PropertyCollector.ObjectSpec 234 | retrieve = si.content.propertyCollector.RetrieveContents 235 | propspec = vmodl.query.PropertyCollector.PropertySpec( 236 | all=False, 237 | pathSet=['storageDeviceInfo'], 238 | type=vim.host.StorageSystem, 239 | ) 240 | 241 | objs = [ObjectSpec(obj=host['props']['configManager'].storageSystem)] 242 | 243 | 244 | filter_spec = vmodl.query.PropertyCollector.FilterSpec( 245 | objectSet = objs, 246 | propSet = [propspec], 247 | ) 248 | 249 | result = retrieve( [filter_spec] ) 250 | storage = process_retrieve_content(result) 251 | return storage[0] 252 | 253 | if __name__ == "__main__": 254 | run() 255 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/vsan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | """ 20 | vsan health 21 | """ 22 | 23 | __cmd__ = "vsan" 24 | 25 | import ssl 26 | from pyVmomi import vim 27 | from ..tools import cli, service_instance 28 | from ..tools.helper import CheckArgument, isallowed, isbanned 29 | from ..tools.helper import find_entity_views, process_retrieve_content 30 | from monplugin import Check, Status 31 | 32 | OK = Status.OK 33 | WARNING = Status.WARNING 34 | CRITICAL = Status.CRITICAL 35 | UNKNOWN = Status.UNKNOWN 36 | 37 | args = None 38 | 39 | # https://kb.vmware.com/s/article/2108319 40 | # https://archive.ph/r7E96 41 | object_health = { 42 | 'datamove': OK, 43 | 'healthy': OK, 44 | 'inaccessible': CRITICAL, 45 | 'nonavailabilityrelatedincompliance': CRITICAL, # should not be possible according to the docs 46 | 'nonavailabilityrelatedincompliancewithpausedrebuild': WARNING, 47 | 'nonavailabilityrelatedincompliancewithpolicypending': OK, 48 | 'nonavailabilityrelatedincompliancewithpolicypendingfailed': CRITICAL, 49 | 'nonavailabilityrelatedreconfig': OK, 50 | 'reducedavailabilitywithactiverebuild': WARNING, # debatable 51 | 'reducedavailabilitywithnorebuild': CRITICAL, 52 | 'reducedavailabilitywithnorebuilddelaytimer': WARNING, 53 | 'reducedavailabilitywithpausedrebuild': CRITICAL, 54 | 'reducedavailabilitywithpolicypending': OK, 55 | 'reducedavailabilitywithpolicypendingfailed': CRITICAL, 56 | 'remoteAccessible': UNKNOWN, # is ignored for now 57 | 'VsanObjectHealthState_Unknown': WARNING, 58 | } 59 | 60 | def run(): 61 | global args 62 | import_vsan() 63 | parser = get_argparser() 64 | args = parser.get_args() 65 | 66 | check = Check() 67 | 68 | args._si = service_instance.connect(args) 69 | 70 | clusters = find_entity_views( 71 | args._si, 72 | vim.ClusterComputeResource, 73 | begin_entity=args._si.content.rootFolder, 74 | properties=['name', 'configurationEx'] 75 | ) 76 | 77 | clusters = process_retrieve_content(list(map(lambda x: x['obj'], clusters))) 78 | 79 | apiVersion = vsu.GetLatestVmodlVersion(args.host, int(args.port)) 80 | vcMos = vsu.GetVsanVcMos( #vsu.GetVsanVcMos( 81 | args._si._stub, 82 | context=sslContext(args), 83 | version=apiVersion 84 | ) 85 | vhs = vcMos['vsan-cluster-health-system'] 86 | 87 | clusters = list(filter(lambda x: x['configurationEx'].vsanConfigInfo.enabled, clusters)) 88 | 89 | for cluster in clusters: 90 | if isbanned(args, cluster['name'], 'exclude'): 91 | continue 92 | if not isallowed(args, cluster['name'], 'include'): 93 | continue 94 | 95 | fields = ['vsanConfig'] 96 | if args.mode == 'objecthealth': 97 | fields.append('objectHealth') 98 | if args.mode == 'healthtest': 99 | fields.append('groups') 100 | 101 | healthSummary = vhs.QueryClusterHealthSummary( 102 | cluster=cluster['moref'], 103 | includeObjUuids=False, 104 | fetchFromCache=args.cache, 105 | fields=fields 106 | ) 107 | 108 | cluster['healthSummary'] = healthSummary 109 | 110 | # filter banned clusters 111 | clusters = list(filter(lambda x: 'healthSummary' in x, clusters)) 112 | 113 | if args.mode == "objecthealth": 114 | check_objecthealth(check, clusters) 115 | elif args.mode == "healthtest": 116 | check_healthtest(check, clusters) 117 | else: 118 | raise Exception("WHAT?") 119 | 120 | def check_healthtest(check, clusters): 121 | for cluster in clusters: 122 | #print((cluster['name'], cluster)) 123 | if not cluster['healthSummary'].vsanConfig.vsanEnabled: 124 | continue 125 | for group in cluster['healthSummary'].groups: 126 | if isbanned(args, group.groupName, 'exclude_group'): 127 | continue 128 | if not isallowed(args, group.groupName, 'include_group'): 129 | continue 130 | for test in group.groupTests: 131 | if isbanned(args, test.testName, 'exclude_test'): 132 | continue 133 | if not isallowed(args, test.testName, 'include_test'): 134 | continue 135 | check.add_message( 136 | health2state(test.testHealth), 137 | f"Cluster: {cluster['moref'].name} Group: { group.groupName } Status: { test.testHealth } Test: { test.testName }" 138 | ) 139 | 140 | opts = {} 141 | if not args.verbose: 142 | opts['allok'] = "everything is fine" 143 | 144 | (status, message) = check.check_messages(separator='\n', separator_all='\n', **opts) 145 | check.exit(status, message) 146 | 147 | 148 | 149 | def check_objecthealth(check, clusters): 150 | for cluster in clusters: 151 | oh = cluster['healthSummary'].objectHealth 152 | if not cluster['healthSummary'].vsanConfig.vsanEnabled: 153 | check.add_message(OK, f"cluster {cluster['name']} doesn't have objectHealth") 154 | continue 155 | 156 | if oh is None: 157 | # cluster doesn't have objectHealth 158 | check.add_message(OK, f"cluster {cluster['name']} doesn't have objectHealth") 159 | continue 160 | 161 | for detail in oh.objectHealthDetail: 162 | check.add_perfdata(label=f"{cluster['name']} {detail.health}", value=detail.numObjects) 163 | 164 | if detail.health == 'remoteAccessible': 165 | # ignore them, should be checked on the remote side 166 | continue 167 | 168 | if detail.numObjects == 0: 169 | continue 170 | 171 | state = object_health.get(detail.health, WARNING) 172 | check.add_message(state, f"there are {detail.numObjects} in state {detail.health} on cluster { cluster['name'] }") 173 | 174 | opts = {} 175 | if not args.verbose: 176 | opts['allok'] = "everything is fine" 177 | 178 | (status, message) = check.check_messages(separator='\n', separator_all='\n', **opts) 179 | check.exit(status, message) 180 | 181 | def sslContext(args): 182 | context = ssl.create_default_context() 183 | context.check_hostname = False 184 | context.verify_mode = ssl.CERT_NONE 185 | 186 | return context 187 | 188 | 189 | def get_argparser(): 190 | parser = cli.Parser() 191 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of cluster', name=['--exclude'])) 192 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of group (only with --mode healthtest)', name=['--exclude-group'])) 193 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of test (only with --mode healthtest)', name=['--exclude-test'])) 194 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of cluster', name=['--include'])) 195 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of group (only with --mode healthtest)', name=['--include-group'])) 196 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of test (only with --mode healthtest)', name=['--include-test'])) 197 | parser.add_optional_arguments({ 198 | 'name_or_flags': ['--cache'], 199 | 'options': { 200 | 'action': 'store_true', 201 | 'help': 'tell the api to return cached data if available' 202 | } 203 | }) 204 | parser.add_required_arguments( { 205 | 'name_or_flags': ['--mode'], 206 | 'options': { 207 | 'action': 'store', 208 | 'choices': [ 209 | 'objecthealth', 210 | 'healthtest', 211 | ], 212 | 'help': 'which runtime mode to check' 213 | } 214 | }) 215 | 216 | return parser 217 | 218 | def import_vsan(): 219 | global vsu 220 | try: 221 | import vsanapiutils as vsu 222 | except Exception as e: 223 | print(( 224 | f"""{str(e)}\n\n""" 225 | "pyVmomi is too old, at least 8.0.3.0.1 is required" 226 | ).strip()) 227 | raise SystemExit(3) 228 | 229 | def health2state(color): 230 | color = color or "" 231 | return { 232 | "green": Status.OK, 233 | "yellow": Status.WARNING, 234 | "red": Status.CRITICAL, 235 | 'unknown': Status.WARNING, 236 | 'info': Status.OK, 237 | 'skipped': Status.OK, 238 | # think about it more if this ever happens, according to the API it could 239 | # but it doesn't say what that means, so we investigate this once we see it 240 | # in the wild 241 | "": Status.WARNING, 242 | }.get(color.lower(), Status.WARNING) 243 | 244 | if __name__ == "__main__": 245 | try: 246 | run() 247 | except SystemExit as e: 248 | if e.code > 3 or e.code < 0: 249 | print("UNKNOWN EXIT CODE") 250 | raise SystemExit(Status.UNKNOWN) 251 | except Exception as e: 252 | print("UNKNOWN - " + str(e)) 253 | raise SystemExit(Status.UNKNOWN) 254 | -------------------------------------------------------------------------------- /LICENSE-VMWARE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 VMware, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /checkvsphere/vcmd/hostruntime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2023 ConSol Consulting & Solutions Software GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | check various runtime info from a host 20 | """ 21 | 22 | __cmd__ = 'host-runtime' 23 | 24 | from pyVmomi import vim 25 | from monplugin import Check, Status 26 | from ..tools import cli, service_instance 27 | from ..tools.helper import find_entity_views, isbanned, isallowed, CheckArgument 28 | 29 | 30 | def run(): 31 | parser = cli.Parser() 32 | # parser.add_optional_arguments(cli.Argument.DATACENTER_NAME) 33 | parser.add_optional_arguments(cli.Argument.VIHOST) 34 | 35 | parser.add_required_arguments( { 36 | 'name_or_flags': ['--mode'], 37 | 'options': { 38 | 'action': 'store', 39 | 'choices': [ 40 | 'con', 41 | 'health', 42 | 'issues', 43 | 'maintenance', 44 | 'status', 45 | 'temp', 46 | 'version', 47 | ], 48 | 'help': 'which runtime mode to check' 49 | } 50 | }) 51 | 52 | parser.add_optional_arguments( { 53 | 'name_or_flags': ['--maintenance-state'], 54 | 'options': { 55 | 'action': 'store', 56 | 'choices': ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'], 57 | 'help': 'exit with this status if the host is in maintenance, ' 58 | 'default UNKNOWN, or CRITICAL if --mode maintenance' 59 | } 60 | }) 61 | 62 | parser.add_optional_arguments(CheckArgument.ALLOWED('regex, name of info item')) 63 | parser.add_optional_arguments(CheckArgument.BANNED('regex, name of info item')) 64 | 65 | args = parser.get_args() 66 | 67 | # default value differs if mode == maintenance 68 | if not args.maintenance_state: 69 | args.maintenance_state = 'CRITICAL' if args.mode == 'maintenance' else 'UNKNOWN' 70 | 71 | si = service_instance.connect(args) 72 | 73 | check = Check() 74 | 75 | #vm_view = si.content.viewManager.CreateContainerView(parentView, [vim.VirtualMachine], True) 76 | try: 77 | vm = find_entity_views( 78 | si, 79 | vim.HostSystem, 80 | begin_entity=si.content.rootFolder, 81 | sieve=({'name': args.vihost} if args.vihost else None), 82 | properties=['name', 'runtime.inMaintenanceMode'], 83 | )[0] 84 | except IndexError: 85 | check.exit(Status.UNKNOWN, f"host {args.vihost or ''} not found") 86 | 87 | result = [] 88 | 89 | if vm['props']['runtime.inMaintenanceMode']: 90 | check.exit( 91 | Status[args.maintenance_state], 92 | f"host {vm['props']['name']} is in maintenance" 93 | ) 94 | 95 | okmessage = "No errors" 96 | 97 | if args.mode == "health": 98 | okmessage = check_health(check, vm, args, result) 99 | elif args.mode == "status": 100 | check_status(check, vm, args, result) 101 | elif args.mode == "con": 102 | check_con(check, vm, args, result) 103 | elif args.mode == "temp": 104 | okmessage = check_temp(check, vm, args, result) 105 | elif args.mode == "issues": 106 | okmessage = check_issues(check, vm, args, result) 107 | elif args.mode == "version": 108 | version = vm['obj'].obj.summary.config.product.fullName 109 | check.exit(Status.OK, version) 110 | elif args.mode == 'maintenance': 111 | check.exit(Status.OK, "Host is not in maintenance") 112 | 113 | opts = {} 114 | if not args.verbose: 115 | opts['allok'] = okmessage 116 | 117 | (code, message) = check.check_messages(separator="\n", separator_all='\n', **opts) 118 | check.exit( 119 | code=code, 120 | message=( message or "everything ok" ) 121 | ) 122 | 123 | def format_issue(issue): 124 | things = [ 125 | # checkfor, name, how to get value 126 | ('datacenter', lambda x: 'Datacenter: ' + x.datacenter.name), 127 | ('host', lambda x: 'Host: ' + x.host.name), 128 | ('vm', lambda x: 'VM: ' + x.vm.name), 129 | ('computeResource', lambda x: 'Compute Resource: ' + x.computeResource.name), 130 | ('dvs', lambda x: 'Virtual Switch: ' + x.dvs.name), 131 | ('ds', lambda x: 'Datastore: ' + x.ds.name), 132 | ('net', lambda x: 'Network: ' + x.net.name), 133 | (None, lambda x: 'Message: ' + x.fullFormattedMessage), 134 | ('userName', lambda x: f'(caused by {x.userName})' if x.userName != "" else None), 135 | ] 136 | 137 | formattedThings = [] 138 | for thing in things: 139 | if thing[0]: 140 | if not ( getattr(issue, thing[0], None) and getattr(issue, thing[0]) ): 141 | continue 142 | 143 | formattedThing = thing[1](issue) 144 | if formattedThing: 145 | formattedThings.append( formattedThing ) 146 | 147 | return ", ".join(formattedThings) 148 | 149 | 150 | def check_issues(check, vm, args, result): 151 | issues = vm['obj'].obj.configIssue 152 | for issue in issues: 153 | if isbanned(args, issue.fullFormattedMessage): 154 | continue 155 | if not isallowed(args, issue.fullFormattedMessage): 156 | continue 157 | check.add_message(Status.CRITICAL, format_issue(issue)) 158 | 159 | return "No issues found" 160 | 161 | 162 | def check_con(check, vm, args, result): 163 | con = vm['obj'].obj.runtime.connectionState 164 | status = Status.OK 165 | if con == "disconnected": 166 | status = Status.WARNING 167 | elif con == "notResponding": 168 | status = Status.CRITICAL 169 | check.exit( 170 | status, 171 | message = f"connection state is '{con}'" 172 | ) 173 | 174 | def check_status(check, vm, args, result): 175 | color = vm['obj'].obj.overallStatus 176 | status = health2state(color) 177 | check.exit(status, f"overall status is {str(color).upper()}") 178 | 179 | def check_temp(check, vm, args, result): 180 | systemRuntime = vm['obj'].obj.runtime.healthSystemRuntime 181 | if not systemRuntime: 182 | check.exit( 183 | Status.UNKNOWN, 184 | "Temperature status unavailable" 185 | ) 186 | 187 | numericinfo = systemRuntime.systemHealthInfo.numericSensorInfo 188 | for info in numericinfo: 189 | if info.sensorType != "temperature": 190 | continue 191 | if isbanned(args, info.name): 192 | continue 193 | if not isallowed(args, info.name): 194 | continue 195 | state = health2state(info.healthState.key) 196 | name = info.name.rstrip(' Temp') 197 | check.add_perfdata(label=name, value=info.currentReading * (10 ** info.unitModifier)) 198 | check.add_message(state, f"{name} is {info.healthState.key}") 199 | 200 | return "All temperature sensors green" 201 | 202 | def check_health(check, vm, args, result): 203 | healthsystem = vm['obj'].obj.runtime.healthSystemRuntime 204 | if not healthsystem: 205 | check.exit( 206 | Status.UNKNOWN, 207 | "system health status not available, " 208 | "no vim.Host.runtime.healthSystemRuntime found" 209 | ) 210 | if not healthsystem.hardwareStatusInfo: 211 | check.exit( 212 | Status.UNKNOWN, 213 | "hardware health information not available, " 214 | "no vim.Host.runtime.healthSystemRuntime.hardwareStatusInfo found" 215 | ) 216 | 217 | filterunknown = lambda x: x.status.key.lower() != "unknown" 218 | cpustatus = healthsystem.hardwareStatusInfo.cpuStatusInfo 219 | storagestatus = list(filter(filterunknown, healthsystem.hardwareStatusInfo.storageStatusInfo)) 220 | memorystatus = list(filter(filterunknown, healthsystem.hardwareStatusInfo.memoryStatusInfo)) 221 | numericsensor = healthsystem.systemHealthInfo.numericSensorInfo 222 | 223 | count = {} 224 | 225 | # "[$status2text{$fstate}] [Type: $type] [Name: $item_ref->{name}] [Label: $item_ref->{label}] [Summary: $item_ref->{summary}]$multiline"; 226 | if memorystatus: 227 | for info in memorystatus: 228 | state = health2state(info.status.key) 229 | if isbanned(args, info.name): 230 | continue 231 | if not isallowed(args, info.name): 232 | continue 233 | if state == Status.UNKNOWN: 234 | continue 235 | check.add_message(state, f"{state.name} [Type: Memory] [Name: { info.name }] [Summary: { info.status.summary }]") 236 | count.setdefault('memory', 0) 237 | count['memory'] += 1 238 | 239 | if cpustatus: 240 | for info in cpustatus: 241 | state = health2state(info.status.key) 242 | if state == Status.UNKNOWN: 243 | # I don't know if this is true, check_vmware_esx said that 244 | check.exit(Status.CRITICAL, 245 | "No result from CIM server regarding health state. " 246 | "CIM server is probably not running or not running correctly! " 247 | "Please restart!" 248 | ) 249 | check.add_message(state, f"{state.name} [Type: CPU] [Name: { info.name }] [Summary: { info.status.summary }]") 250 | count.setdefault('cpu', 0) 251 | count['cpu'] += 1 252 | 253 | if storagestatus: 254 | for info in storagestatus: 255 | if isbanned(args, info.name): 256 | continue 257 | if not isallowed(args, info.name): 258 | continue 259 | state = health2state(info.status.key) 260 | if state == Status.UNKNOWN: 261 | continue 262 | 263 | check.add_message(state, f"{state.name} [Type: Storage] [Name: { info.name }] [Summary: { info.status.summary }]") 264 | count.setdefault('storage', 0) 265 | count['storage'] += 1 266 | 267 | 268 | if numericsensor: 269 | for info in numericsensor: 270 | if info.sensorType == "Software Components": 271 | # It is said they make no sense 272 | continue 273 | if isbanned(args, info.name): 274 | continue 275 | if not isallowed(args, info.name): 276 | continue 277 | if 'unknown' in info.healthState.label and 'Cannot report' in info.healthState.summary: 278 | # Filter out sensors which have no valid data. Often a sensor is recognized by vmware 279 | # but has not the ability to report something senseful. So it can be skipped. 280 | continue 281 | 282 | state = health2state(info.healthState.key) 283 | if state == Status.UNKNOWN: 284 | continue 285 | 286 | check.add_message(state, 287 | f"{state.name} [Type: {info.sensorType}] " 288 | f"[Name: {info.name}] [Label: {info.healthState.label}] " 289 | f"[Summary: {info.healthState.summary}]" 290 | ) 291 | count.setdefault(str(info.sensorType), 0) 292 | count[str(info.sensorType)] += 1 293 | 294 | okmessage = ( 295 | f"All {sum(count.values())} health checks are GREEN: " + 296 | (', '.join(list( f"{x}: {count[x]}" for x in count ))) 297 | ) 298 | return okmessage 299 | 300 | def health2state(color): 301 | return { 302 | "green": Status.OK, 303 | "yellow": Status.WARNING, 304 | "red": Status.CRITICAL, 305 | }.get(color.lower(), Status.UNKNOWN) 306 | 307 | if __name__ == "__main__": 308 | run() 309 | -------------------------------------------------------------------------------- /checkvsphere/tools/cli.py: -------------------------------------------------------------------------------- 1 | # VMware vSphere Python SDK Community Samples Addons 2 | # Copyright (c) 2014-2021 VMware, Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | This module implements simple helper functions for python samples 18 | """ 19 | import argparse 20 | import getpass 21 | import os 22 | 23 | __author__ = "VMware, Inc." 24 | 25 | 26 | class EnvDefault(argparse.Action): 27 | def __init__(self, envvar, required=True, default=None, **kwargs): 28 | if not default and envvar: 29 | if os.environ.get(envvar, None): 30 | default = os.environ[envvar] 31 | if required and default: 32 | required = False 33 | super(EnvDefault, self).__init__(default=default, required=required, **kwargs) 34 | 35 | def __call__(self, parser, namespace, values, option_string=None): 36 | setattr(namespace, self.dest, values) 37 | 38 | 39 | class Parser: 40 | """ 41 | Samples specific argument parser. 42 | Wraps argparse to ease the setup of argument requirements for the samples. 43 | 44 | Example: 45 | parser = cli.Parser() 46 | parser.add_required_arguments(cli.Argument.VM_NAME) 47 | parser.add_optional_arguments(cli.Argument.DATACENTER_NAME, cli.Argument.NIC_NAME) 48 | parser.add_custom_argument( 49 | '--disk-number', required=True, help='Disk number to change mode.') 50 | args = parser.get_args() 51 | """ 52 | 53 | def __init__(self): 54 | """ 55 | Defines two arguments groups. 56 | One for the standard arguments and one for sample specific arguments. 57 | The standard group cannot be extended. 58 | """ 59 | self._parser = argparse.ArgumentParser(description='Arguments for talking to vCenter') 60 | self._standard_args_group = self._parser.add_argument_group('standard arguments') 61 | self._specific_args_group = self._parser.add_argument_group('sample-specific arguments') 62 | 63 | # because -h is reserved for 'help' we use -s for service 64 | self._standard_args_group.add_argument('-s', '--host', 65 | required=True, 66 | action='store', 67 | help='vSphere service address to connect to') 68 | 69 | # because we want -p for password, we use -o for port 70 | self._standard_args_group.add_argument('-o', '--port', 71 | type=int, 72 | default=443, 73 | action='store', 74 | help='Port to connect on') 75 | 76 | self._standard_args_group.add_argument('-u', '--user', 77 | required=True, 78 | action='store', 79 | help='User name to use when connecting to host') 80 | 81 | self._standard_args_group.add_argument('-p', '--password', 82 | required=True, 83 | action=EnvDefault, 84 | envvar='VSPHERE_PASS', 85 | help='Password to use when connecting to host, ' 86 | 'can also be set by env VSPHERE_PASS') 87 | 88 | self._standard_args_group.add_argument('-nossl', '--disable-ssl-verification', 89 | required=False, 90 | action='store_true', 91 | help='Disable ssl host certificate verification') 92 | 93 | self._standard_args_group.add_argument('-v', '--verbose', 94 | required=False, 95 | default=0, 96 | action='count', 97 | help='verbosity') 98 | 99 | self._standard_args_group.add_argument('--sessionfile', 100 | required=False, 101 | action='store', 102 | help='Path to a file where the sessioncookie' 103 | 'will be stored for later reuse. (EXPERIMENTAL)') 104 | 105 | self._standard_args_group.add_argument('--match-method', 106 | required=False, 107 | action='store', 108 | default='search', 109 | choices=['search', 'match', 'fullmatch'], 110 | help='changes the behaviour of regex for --allow and --banned, defaults to search. ' 111 | 'https://docs.python.org/3/library/re.html#search-vs-match' 112 | ) 113 | 114 | def get_args(self): 115 | """ 116 | Supports the command-line arguments needed to form a connection to vSphere. 117 | """ 118 | args = self._parser.parse_args() 119 | return args 120 | 121 | def _add_sample_specific_arguments(self, is_required: bool, *args): 122 | """ 123 | Add an argument to the "sample specific arguments" group 124 | Requires a predefined argument from the Argument class. 125 | """ 126 | for arg in args: 127 | name_or_flags = arg["name_or_flags"] 128 | options = arg["options"] 129 | options["required"] = is_required 130 | self._specific_args_group.add_argument(*name_or_flags, **options) 131 | 132 | def add_required_arguments(self, *args): 133 | """ 134 | Add a required argument to the "sample specific arguments" group 135 | Requires a predefined argument from the Argument class. 136 | """ 137 | self._add_sample_specific_arguments(True, *args) 138 | 139 | def add_optional_arguments(self, *args): 140 | """ 141 | Add an optional argument to the "sample specific arguments" group. 142 | Requires a predefined argument from the Argument class. 143 | """ 144 | self._add_sample_specific_arguments(False, *args) 145 | 146 | def add_custom_argument(self, *name_or_flags, **options): 147 | """ 148 | Uses ArgumentParser.add_argument() to add a full definition of a command line argument 149 | to the "sample specific arguments" group. 150 | https://docs.python.org/3/library/argparse.html#the-add-argument-method 151 | """ 152 | self._specific_args_group.add_argument(*name_or_flags, **options) 153 | 154 | def set_epilog(self, epilog): 155 | """ 156 | Text to display after the argument help 157 | """ 158 | self._parser.epilog = epilog 159 | 160 | def _prompt_for_password(self, args): 161 | """ 162 | if no password is specified on the command line, prompt for it 163 | """ 164 | if not args.password: 165 | args.password = getpass.getpass( 166 | prompt='"--password" not provided! Please enter password for host %s and user %s: ' 167 | % (args.host, args.user)) 168 | return args 169 | 170 | 171 | class Argument: 172 | """ 173 | Predefined arguments to use in the Parser 174 | 175 | Example: 176 | parser = cli.Parser() 177 | parser.add_optional_arguments(cli.Argument.VM_NAME) 178 | parser.add_optional_arguments(cli.Argument.DATACENTER_NAME, cli.Argument.NIC_NAME) 179 | """ 180 | 181 | def __init__(self): 182 | pass 183 | 184 | UUID = { 185 | 'name_or_flags': ['--uuid'], 186 | 'options': {'action': 'store', 'help': 'UUID of an entity (VirtualMachine or HostSystem)'} 187 | } 188 | VM_NAME = { 189 | 'name_or_flags': ['--vm-name'], 190 | 'options': {'action': 'store', 'help': 'Name of the vm'} 191 | } 192 | VM_IP = { 193 | 'name_or_flags': ['--vm-ip'], 194 | 'options': {'action': 'store', 'help': 'IP of the vm'} 195 | } 196 | VM_MAC = { 197 | 'name_or_flags': ['-mac', '--vm-mac'], 198 | 'options': {'action': 'store', 'help': 'Mac address of the VM'} 199 | } 200 | VM_USER = { 201 | 'name_or_flags': ['--vm-user'], 202 | 'options': {'action': 'store', 'help': 'virtual machine user name'} 203 | } 204 | VM_PASS = { 205 | 'name_or_flags': ['--vm-password'], 206 | 'options': {'action': 'store', 'help': 'virtual machine password'} 207 | } 208 | ESX_NAME = { 209 | 'name_or_flags': ['-e', '--esx-name'], 210 | 'options': {'action': 'store', 'help': 'Esx name'} 211 | } 212 | ESX_IP = { 213 | 'name_or_flags': ['--esx-ip'], 214 | 'options': {'action': 'store', 'help': 'Esx ip'} 215 | } 216 | ESX_NAME_REGEX = { 217 | 'name_or_flags': ['--esx-name-regex'], 218 | 'options': {'action': 'store', 'help': 'Esx name regex'} 219 | } 220 | DNS_NAME = { 221 | 'name_or_flags': ['--dns-name'], 222 | 'options': {'action': 'store', 'help': 'DNS name'} 223 | } 224 | NAME = { 225 | 'name_or_flags': ['-n', '--name'], 226 | 'options': {'action': 'store', 'help': 'Name of the entity'} 227 | } 228 | NEW_NAME = { 229 | 'name_or_flags': ['-r', '--new-name'], 230 | 'options': {'action': 'store', 'help': 'New name of the entity.'} 231 | } 232 | DATACENTER_NAME = { 233 | 'name_or_flags': ['--datacenter-name'], 234 | 'options': {'action': 'store', 'help': 'Datacenter name'} 235 | } 236 | DATASTORE_NAME = { 237 | 'name_or_flags': ['--datastore-name'], 238 | 'options': {'action': 'store', 'help': 'Datastore name'} 239 | } 240 | CLUSTER_NAME = { 241 | 'name_or_flags': ['--cluster-name'], 242 | 'options': {'action': 'store', 'help': 'Cluster name'} 243 | } 244 | FOLDER_NAME = { 245 | 'name_or_flags': ['--folder-name'], 246 | 'options': {'action': 'store', 'help': 'Folder name'} 247 | } 248 | TEMPLATE = { 249 | 'name_or_flags': ['--template'], 250 | 'options': {'action': 'store', 'help': 'Name of the template/VM'} 251 | } 252 | VMFOLDER = { 253 | 'name_or_flags': ['--vm-folder'], 254 | 'options': {'action': 'store', 'help': 'Name of the VMFolder'} 255 | } 256 | DATASTORECLUSTER_NAME = { 257 | 'name_or_flags': ['--datastorecluster-name'], 258 | 'options': {'action': 'store', 'help': 'Datastorecluster (DRS Storagepod)'} 259 | } 260 | RESOURCE_POOL = { 261 | 'name_or_flags': ['--resource-pool'], 262 | 'options': {'action': 'store', 'help': 'Resource pool name'} 263 | } 264 | POWER_ON = { 265 | 'name_or_flags': ['--power-on'], 266 | 'options': {'action': 'store_true', 'help': 'power on the VM'} 267 | } 268 | LANGUAGE = { 269 | 'name_or_flags': ['--language'], 270 | 'options': {'action': 'store', 'default': 'English', 'help': 'Language your vcenter used.'} 271 | } 272 | VIHOST = { 273 | 'name_or_flags': ['--vihost'], 274 | 'options': {'action': 'store', 275 | 'help': 'Name/ip address of ESXi host as seen in vCenter Server'} 276 | } 277 | DVS_PORT_GROUP_NAME = { 278 | 'name_or_flags': ['--dvs-pg-name'], 279 | 'options': {'action': 'store', 'help': '"Name of the distributed port group'} 280 | } 281 | DVS_NAME = { 282 | 'name_or_flags': ['--dvs-name'], 283 | 'options': {'action': 'store', 'help': 'Name of the distributed virtual switch'} 284 | } 285 | OPAQUE_NETWORK_NAME = { 286 | 'name_or_flags': ['--opaque-network-name'], 287 | 'options': {'action': 'store', 'help': 'Name of an opaque network'} 288 | } 289 | FIRST_CLASS_DISK_NAME = { 290 | 'name_or_flags': ['--fcd-name'], 291 | 'options': {'action': 'store', 'help': 'First Class Disk name'} 292 | } 293 | DISK_TYPE = { 294 | 'name_or_flags': ['--disk-type'], 295 | 'options': {'action': 'store', 296 | 'default': 'thin', 'choices': ['thick', 'thin'], 'help': 'thick or thin'} 297 | } 298 | DISK_SIZE = { 299 | 'name_or_flags': ['--disk-size'], 300 | 'options': {'action': 'store', 'help': 'disk size, in GB, to add to the VM'} 301 | } 302 | PORT_GROUP = { 303 | 'name_or_flags': ['-g', '--port-group'], 304 | 'options': {'action': 'store', 'help': 'Name of port group'} 305 | } 306 | NETWORK_NAME = { 307 | 'name_or_flags': ['--network-name'], 308 | 'options': {'action': 'store', 'help': 'Name of network'} 309 | } 310 | VSWITCH_NAME = { 311 | 'name_or_flags': ['-w', '--vswitch-name'], 312 | 'options': {'action': 'store', 'help': 'vSwitch name'} 313 | } 314 | LOCAL_FILE_PATH = { 315 | 'name_or_flags': ['--local-file-path'], 316 | 'options': {'action': 'store', 'help': 'Local disk path to file'} 317 | } 318 | REMOTE_FILE_PATH = { 319 | 'name_or_flags': ['--remote-file-path'], 320 | 'options': {'action': 'store', 'help': 'Path on datastore or vm or other entity to file'} 321 | } 322 | VLAN_ID = { 323 | 'name_or_flags': ['--vlan-id'], 324 | 'options': {'action': 'store', 'help': 'Vlan ID'} 325 | } 326 | DEVICE_NAME = { 327 | 'name_or_flags': ['--device-name'], 328 | 'options': {'action': 'store', 'help': 'The device name. Might look like ' 329 | '"/vmfs/devices/disks/naa.*". ' 330 | 'See vim.vm.device.VirtualDisk.' 331 | 'RawDiskMappingVer1BackingInfo documentation.'}} 332 | DISK_MODE = { 333 | 'name_or_flags': ['--disk-mode'], 334 | 'options': {'action': 'store', 335 | 'default': 'independent_persistent', 336 | 'choices': [ 337 | 'append', 338 | 'independent_nonpersistent', 339 | 'independent_persistent', 340 | 'nonpersistent', 341 | 'persistent', 342 | 'undoable'], 343 | 'help': 'See vim.vm.device.VirtualDiskOption.DiskMode documentation.'}} 344 | 345 | COMPATIBILITY_MODE = { 346 | 'name_or_flags': ['--disk-compatibility-mode'], 347 | 'options': {'action': 'store', 348 | 'default': 'virtualMode', 349 | 'choices': ['physicalMode', 'virtualMode'], 350 | 'help': 'See vim.vm.device.VirtualDiskOption.CompatibilityMode documentation.'}} 351 | 352 | ISO = { 353 | 'name_or_flags': ['--iso'], 354 | 'options': {'action': 'store', 355 | 'help': 'ISO to use in test. Use datastore path format. ' 356 | 'E.g. [datastore1] path/to/file.iso'} 357 | } 358 | NIC_NAME = { 359 | 'name_or_flags': ['--nic-name'], 360 | 'options': {'action': 'store', 'help': 'NIC number.'} 361 | } 362 | NIC_UNIT_NUMBER = { 363 | 'name_or_flags': ['--nic-unitnumber'], 364 | 'options': {'action': 'store', 'type': int, 'help': 'NIC number.'} 365 | } 366 | NIC_STATE = { 367 | 'name_or_flags': ['--nic-state'], 368 | 'options': {'action': 'store', 'choices': ['delete', 'disconnect', 'connect'], 369 | 'help': 'NIC number.'} 370 | } 371 | VMDK_PATH = { 372 | 'name_or_flags': ['--vmdk-path'], 373 | 'options': {'action': 'store', 'help': 'Path of the VMDK file.'} 374 | } 375 | OVA_PATH = { 376 | 'name_or_flags': ['--ova-path'], 377 | 'options': {'action': 'store', 'help': 'Path to the OVA file.'} 378 | } 379 | OVF_PATH = { 380 | 'name_or_flags': ['--ovf-path'], 381 | 'options': {'action': 'store', 'help': 'Path to the OVF file.'} 382 | } 383 | DATE = { 384 | 'name_or_flags': ['--date'], 385 | 'options': {'action': 'store', 'help': 'Date and time with the format d/m/Y H:M'} 386 | } 387 | MINUTES = { 388 | 'name_or_flags': ['--minutes'], 389 | 'options': {'action': 'store', 'help': 'time in minutes'} 390 | } 391 | MESSAGE = { 392 | 'name_or_flags': ['-m', '--message'], 393 | 'options': {'action': 'store', 'help': 'Message'} 394 | } 395 | 396 | SNAPSHOT_OPERATION = { 397 | 'name_or_flags': ['-op', '--snapshot-operation'], 398 | 'options': {'action': 'store', 399 | 'choices': 400 | ['create', 'remove', 'revert', 'list_all', 'list_current', 'remove_all'], 401 | 'help': 'Snapshot operation'} 402 | } 403 | SNAPSHOT_NAME = { 404 | 'name_or_flags': ['--snapshot-name'], 405 | 'options': {'action': 'store', 'help': 'Snapshot name'} 406 | } 407 | STORAGE_POLICY_NAME = { 408 | 'name_or_flags': ['--storage-policy-name'], 409 | 'options': {'action': 'store', 'metavar': 'string', 'help': 'Storage policy name'} 410 | } 411 | ASSUME_INPUT = { 412 | 'name_or_flags': ['--assume-input'], 413 | 'options': {'action': 'store', 'help': 'Assume user input'} 414 | } 415 | SSL_KEY = { 416 | 'name_or_flags': ['--ssl-key'], 417 | 'options': {'action': 'store', 'help': 'absolute location of the private key file'} 418 | } 419 | SSL_CERT = { 420 | 'name_or_flags': ['--ssl-cert'], 421 | 'options': {'action': 'store', 'help': 'absolute location of the certificate file'} 422 | } 423 | 424 | 425 | def prompt_y_n_question(question, default="no"): 426 | """ based on: 427 | http://code.activestate.com/recipes/577058/ 428 | :param question: Question to ask 429 | :param default: No 430 | :return: True/False 431 | """ 432 | valid = {"yes": True, "y": True, "ye": True, 433 | "no": False, "n": False} 434 | if default is None: 435 | prompt = " [y/n] " 436 | elif default == "yes": 437 | prompt = " [Y/n] " 438 | elif default == "no": 439 | prompt = " [y/N] " 440 | else: 441 | raise ValueError("Invalid default answer: '{}'".format(default)) 442 | 443 | while True: 444 | print(question + prompt) 445 | choice = input().lower() 446 | if default is not None and choice == '': 447 | return valid[default] 448 | elif choice in valid: 449 | return valid[choice] 450 | else: 451 | print("Please, respond with 'yes' or 'no' or 'y' or 'n'.") 452 | --------------------------------------------------------------------------------